@shopgate/pwa-common 7.30.0-alpha.8 → 7.30.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { shallow } from 'enzyme';
3
+ import { act } from 'react-dom/test-utils';
3
4
  import CountdownTimer, { getFormattedTimeString } from "./index";
4
5
  describe('<CountdownTimer>', () => {
5
6
  jest.useFakeTimers();
@@ -36,24 +37,23 @@ describe('<CountdownTimer>', () => {
36
37
  */
37
38
  const performFormatCheck = (remainingDays, remainingHours, remainingMinutes, remainingSeconds) => {
38
39
  jest.clearAllTimers();
39
- setInterval.mock.calls = [];
40
+ const intervalSpy = jest.spyOn(global, 'setInterval');
40
41
  const wrapper = createTimerElement(remainingDays, remainingHours, remainingMinutes, remainingSeconds, null);
41
- const expectedTimeFormat = getFormattedTimeString(remainingDays, remainingHours, remainingMinutes, remainingSeconds - 1 // Expect the decremented timeout.
42
- );
43
-
44
- // We cannot perform a snapshot match here because the timestamp changes for each call.
45
- expect(setInterval.mock.calls.length).toBe(1);
46
- jest.runTimersToTime(1000);
42
+ const expectedTimeFormat = getFormattedTimeString(remainingDays, remainingHours, remainingMinutes, remainingSeconds - 1);
43
+ expect(intervalSpy).toHaveBeenCalledTimes(1);
44
+ act(() => {
45
+ jest.advanceTimersByTime(1000);
46
+ });
47
47
  wrapper.update();
48
48
  const {
49
49
  params,
50
50
  string
51
51
  } = wrapper.props();
52
- const renderedTimeFormat = {
52
+ expect({
53
53
  params,
54
54
  string
55
- };
56
- expect(renderedTimeFormat).toEqual(expectedTimeFormat);
55
+ }).toEqual(expectedTimeFormat);
56
+ intervalSpy.mockRestore();
57
57
  };
58
58
  it('should render the correct time for < 24h', () => performFormatCheck(0, 0, 0, 5));
59
59
  it('should render the correct time for 24h - 48h', () => performFormatCheck(1, 12, 6, 5));
@@ -62,46 +62,44 @@ describe('<CountdownTimer>', () => {
62
62
  jest.clearAllTimers();
63
63
  const wrapper = createTimerElement(-1, -2, -3, -5, null);
64
64
  const expectedTimeFormat = getFormattedTimeString(0, 0, 0, 0);
65
- jest.runTimersToTime(1000);
65
+ act(() => {
66
+ jest.advanceTimersByTime(1000);
67
+ });
66
68
  const {
67
69
  params,
68
70
  string
69
71
  } = wrapper.props();
70
- const renderedTimeFormat = {
72
+ expect({
71
73
  params,
72
74
  string
73
- };
74
- expect(renderedTimeFormat).toEqual(expectedTimeFormat);
75
+ }).toEqual(expectedTimeFormat);
75
76
  });
76
77
  it('should stop at 00:00:00 when the timer expires', () => {
77
78
  const wrapper = createTimerElement(0, 0, 0, 1, null);
78
79
  const expectedTimeFormat = getFormattedTimeString(0, 0, 0, 0);
79
- let renderedTimeFormat;
80
-
81
- // Run down to 00:00:00.
82
- jest.runTimersToTime(1000);
80
+ act(() => {
81
+ jest.advanceTimersByTime(1000);
82
+ });
83
83
  wrapper.update();
84
84
  let {
85
85
  params,
86
86
  string
87
87
  } = wrapper.props();
88
- renderedTimeFormat = {
88
+ expect({
89
89
  params,
90
90
  string
91
- };
92
- expect(renderedTimeFormat).toEqual(expectedTimeFormat);
93
-
94
- // Advance time a bit further and make sure the timer stays at 00:00:00.
95
- jest.runTimersToTime(1000);
91
+ }).toEqual(expectedTimeFormat);
92
+ act(() => {
93
+ jest.advanceTimersByTime(1000);
94
+ });
96
95
  ({
97
96
  params,
98
97
  string
99
98
  } = wrapper.props());
100
- renderedTimeFormat = {
99
+ expect({
101
100
  params,
102
101
  string
103
- };
104
- expect(renderedTimeFormat).toEqual(expectedTimeFormat);
102
+ }).toEqual(expectedTimeFormat);
105
103
  });
106
104
  it('should invoke the callback when the timer expires', () => {
107
105
  let timesCallbackInvoked = 0;
@@ -112,17 +110,17 @@ describe('<CountdownTimer>', () => {
112
110
  timesCallbackInvoked += 1;
113
111
  };
114
112
  createTimerElement(0, 0, 0, 2, callback);
115
-
116
- // The timer is not expired yet. Make sure the callback is not invoked.
117
- jest.runTimersToTime(1000);
113
+ act(() => {
114
+ jest.advanceTimersByTime(1000);
115
+ });
118
116
  expect(timesCallbackInvoked).toBe(0);
119
-
120
- // The timer should expire by now.
121
- jest.runTimersToTime(1000);
117
+ act(() => {
118
+ jest.advanceTimersByTime(1000);
119
+ });
122
120
  expect(timesCallbackInvoked).toBe(1);
123
-
124
- // Run it again and make sure it won't be called twice.
125
- jest.runTimersToTime(1000);
121
+ act(() => {
122
+ jest.advanceTimersByTime(1000);
123
+ });
126
124
  expect(timesCallbackInvoked).toBe(1);
127
125
  });
128
126
  it('should not invoke the callback when the timeout is already expired.', () => {
@@ -134,7 +132,9 @@ describe('<CountdownTimer>', () => {
134
132
  timesCallbackInvoked += 1;
135
133
  };
136
134
  createTimerElement(0, 0, 0, 0, callback);
137
- jest.runTimersToTime(1000);
135
+ act(() => {
136
+ jest.advanceTimersByTime(1000);
137
+ });
138
138
  expect(timesCallbackInvoked).toBe(0);
139
139
  });
140
140
  });
@@ -1,7 +1,8 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
+ /** @jest-environment jsdom */
3
+
2
4
  import React from 'react';
3
5
  import { mount } from 'enzyme';
4
- import { JSDOM } from 'jsdom';
5
6
  import { embeddedMedia } from '@shopgate/pwa-common/collections';
6
7
  import HtmlSanitizer from "./index";
7
8
  jest.mock("../EmbeddedMedia", () => ({
@@ -12,7 +13,7 @@ jest.mock("./connector", () => Cmp => Cmp);
12
13
  /**
13
14
  * @param {string} html HTML markup.
14
15
  * @param {Object} props Component props.
15
- * @returns {JSX}
16
+ * @returns {JSX.Element}
16
17
  */
17
18
  const createWrapper = (html, props = {}) => mount(/*#__PURE__*/React.createElement(HtmlSanitizer, _extends({
18
19
  navigate: () => {}
@@ -125,7 +126,9 @@ describe('<HtmlSanitizer />', () => {
125
126
  mockedHandleClick.mockClear();
126
127
  });
127
128
  it('follows a link from a plain <a>', () => {
128
- const doc = new JSDOM('<!doctype html><html><body><div>/<div></body></html>').window.document;
129
+ // Create a real container element in the current document
130
+ const attachNode = document.createElement('div');
131
+ document.body.appendChild(attachNode);
129
132
  const html = '&lt;a id=&quot;link&quot; href=&quot;#follow-me-and-everything-is-alright&quot;&gt;Plain Link&lt;/a&gt;';
130
133
  const wrapper = mount(/*#__PURE__*/React.createElement(HtmlSanitizer, {
131
134
  decode: true,
@@ -134,9 +137,9 @@ describe('<HtmlSanitizer />', () => {
134
137
  },
135
138
  navigate: () => {}
136
139
  }, html), {
137
- attachTo: doc.getElementsByTagName('div')[0]
140
+ attachTo: attachNode
138
141
  });
139
- const aTag = doc.getElementsByTagName('a')[0];
142
+ const aTag = attachNode.getElementsByTagName('a')[0];
140
143
  aTag.closest = () => aTag;
141
144
  const event = {
142
145
  target: aTag,
@@ -145,9 +148,12 @@ describe('<HtmlSanitizer />', () => {
145
148
  wrapper.instance().handleTap(event);
146
149
  expect(mockedHandleClick).toHaveBeenCalledTimes(1);
147
150
  expect(mockedHandleClick).toHaveBeenCalledWith('#follow-me-and-everything-is-alright', '');
151
+ wrapper.unmount();
152
+ attachNode.remove();
148
153
  });
149
154
  it('follows a link from a <a> with other HTML inside', () => {
150
- const doc = new JSDOM('<!doctype html><html><body><div>/<div></body></html>').window.document;
155
+ const attachNode = document.createElement('div');
156
+ document.body.appendChild(attachNode);
151
157
  const html = '&lt;a id=&quot;link&quot; target=&quot;_blank&quot; href=&quot;#I-ll-be-the-one-to-tuck-you-in-at-night&quot;&gt;&lt;span&gt;Span Link&lt;/span&gt;&lt;/a&gt;';
152
158
  const wrapper = mount(/*#__PURE__*/React.createElement(HtmlSanitizer, {
153
159
  decode: true,
@@ -156,10 +162,10 @@ describe('<HtmlSanitizer />', () => {
156
162
  },
157
163
  navigate: () => {}
158
164
  }, html), {
159
- attachTo: doc.getElementsByTagName('div')[0]
165
+ attachTo: attachNode
160
166
  });
161
- const aTag = doc.getElementsByTagName('a')[0];
162
- const spanTag = doc.getElementsByTagName('span')[0];
167
+ const aTag = attachNode.getElementsByTagName('a')[0];
168
+ const spanTag = attachNode.getElementsByTagName('span')[0];
163
169
  spanTag.closest = () => aTag;
164
170
  const event = {
165
171
  target: spanTag,
@@ -1,6 +1,10 @@
1
1
  import React from 'react';
2
2
  import { shallow } from 'enzyme';
3
3
  import SwiperItem from '.';
4
+ jest.mock('react', () => ({
5
+ ...jest.requireActual('react'),
6
+ useLayoutEffect: jest.requireActual('react').useEffect
7
+ }));
4
8
  describe('<SwiperItem />', () => {
5
9
  it('should not render without children', () => {
6
10
  const wrapper = shallow(/*#__PURE__*/React.createElement(SwiperItem, null, /*#__PURE__*/React.createElement("div", null)));
@@ -3,7 +3,7 @@ import { mount } from 'enzyme';
3
3
  import Grid from "../../../Grid";
4
4
  import Widget from "./index";
5
5
  jest.mock('react', () => ({
6
- ...require.requireActual('react'),
6
+ ...jest.requireActual('react'),
7
7
  Suspense: function Suspense({
8
8
  children
9
9
  }) {
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { mount } from 'enzyme';
3
3
  import WidgetGrid from "./index";
4
4
  jest.mock('react', () => ({
5
- ...require.requireActual('react'),
5
+ ...jest.requireActual('react'),
6
6
  Suspense: function Suspense({
7
7
  children
8
8
  }) {
@@ -137,10 +137,16 @@ describe('<Widgets />', () => {
137
137
  expect(wrapper.find('img').length).toBe(1);
138
138
  });
139
139
  it('should schedule a re-render when widget is scheduled', () => {
140
- const minutesToNextFullHour = 60 - new Date().getMinutes();
141
- const msToNextFullHour = minutesToNextFullHour * 60000;
140
+ // Use a fixed point in time so "next full hour" is deterministic.
141
+ // Pick a time that's not exactly on the hour.
142
+ const base = new Date('2023-01-01T10:37:00.000Z');
143
+ jest.setSystemTime(base);
144
+ const minutesToNextFullHour = 60 - base.getMinutes(); // 23
145
+ const msToNextFullHour = minutesToNextFullHour * 60000; // 23 * 60_000
146
+
142
147
  const scheduledFromMs = Date.now() + msToNextFullHour - 1;
143
148
  const scheduledToMs = Date.now() + minutesToNextFullHour + 1000;
149
+
144
150
  /* eslint-disable camelcase */
145
151
  const widgets = [{
146
152
  col: 0,
@@ -159,18 +165,27 @@ describe('<Widgets />', () => {
159
165
  type: '@shopgate/commerce-widgets/image'
160
166
  }];
161
167
  /* eslint-enable camelcase */
168
+
162
169
  const wrapper = createWrapper(widgets);
163
170
  const instance = wrapper.find('Widgets').instance();
171
+ const clearSpy = jest.spyOn(global, 'clearTimeout');
164
172
  instance.forceUpdate = jest.fn();
173
+
174
+ // Before the schedule hits, the image should not render yet.
165
175
  expect(wrapper.find(Image).exists()).toBe(false);
176
+
177
+ // 1) Advance to the next full hour -> first forced update.
166
178
  jest.advanceTimersByTime(msToNextFullHour);
167
179
  expect(instance.forceUpdate).toHaveBeenCalledTimes(1);
168
- // In real life next timeout should be in 60 minutes.
169
- // This test has same Date and fake timers.
170
- jest.advanceTimersByTime(msToNextFullHour);
180
+
181
+ // 2) The component should schedule the next tick for +60 min.
182
+ jest.advanceTimersByTime(60 * 60000);
171
183
  expect(instance.forceUpdate).toHaveBeenCalledTimes(2);
172
- instance.componentWillUnmount();
173
- expect(clearTimeout).toHaveBeenCalled();
184
+
185
+ // Unmount triggers cleanup of any pending timeouts.
186
+ wrapper.unmount();
187
+ expect(clearSpy).toHaveBeenCalled();
188
+ clearSpy.mockRestore();
174
189
  });
175
190
  it('should render only wrapper when widgets array is empty', () => {
176
191
  const widgets = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopgate/pwa-common",
3
- "version": "7.30.0-alpha.8",
3
+ "version": "7.30.0-alpha.9",
4
4
  "description": "Common library for the Shopgate Connect PWA.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Shopgate <support@shopgate.com>",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "@redux-devtools/extension": "^3.3.0",
19
19
  "@sentry/browser": "6.0.1",
20
- "@shopgate/pwa-benchmark": "7.30.0-alpha.8",
20
+ "@shopgate/pwa-benchmark": "7.30.0-alpha.9",
21
21
  "@virtuous/conductor": "~2.5.0",
22
22
  "@virtuous/react-conductor": "~2.5.0",
23
23
  "@virtuous/redux-persister": "1.1.0-beta.7",
@@ -27,7 +27,6 @@
27
27
  "gsap": "^3.6.0",
28
28
  "history": "^4.9.0",
29
29
  "intl-messageformat": "^7.8.3",
30
- "jsdom": "11.12.0",
31
30
  "path-match": "^1.2.4",
32
31
  "react-dotdotdot": "~1.3.0",
33
32
  "react-helmet": "^6.1.0",
@@ -42,7 +41,7 @@
42
41
  "url-search-params": "^0.10.0"
43
42
  },
44
43
  "devDependencies": {
45
- "@shopgate/pwa-core": "7.30.0-alpha.8",
44
+ "@shopgate/pwa-core": "7.30.0-alpha.9",
46
45
  "@types/react-portal": "^3.0.9",
47
46
  "lodash": "^4.17.21",
48
47
  "prop-types": "~15.8.1",