@shopgate/pwa-unit-test 7.32.0-beta.1 → 7.32.0-beta.10

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.
package/README.md CHANGED
@@ -66,12 +66,69 @@ const defaultConfig = require('@shopgate/pwa-unit-test/jest.config');
66
66
  module.exports = {
67
67
  ...defaultConfig,
68
68
  moduleNameMapper: {
69
- '^Components(.*)$': '<rootDir>/components',
70
- '^Styles(.*)$': '<rootDir>/styles',
69
+ // Keep the default mappings - they would be replaced otherwise
70
+ ...defaultConfig.moduleNameMapper,
71
+ '^Components(.*)$': '<rootDir>/components$1',
72
+ '^Styles(.*)$': '<rootDir>/styles$1',
71
73
  },
72
74
  };
73
75
  ```
74
76
 
77
+ __NOTE: Options which hold an object or an array - like `moduleNameMapper`, `setupFiles` or
78
+ `transformIgnorePatterns` - are replaced and not merged when they are set. Spread the value of the
79
+ default configuration first to keep it, as shown above. Without that, the mappings for styles and
80
+ assets are lost, and imports of stylesheets or images within the tested components fail.__
81
+
82
+ ## Test utilities
83
+
84
+ ### `createMockStore`
85
+
86
+ Creates a minimal Redux store for component tests. It implements the contract that `react-redux`
87
+ relies on, without pulling in the reducers of the application:
88
+
89
+ ```js
90
+ import { createMockStore } from '@shopgate/pwa-unit-test/testUtils';
91
+
92
+ const store = createMockStore({ counter: 0 });
93
+
94
+ mount(
95
+ <Provider store={store}>
96
+ <MyComponent />
97
+ </Provider>
98
+ );
99
+ ```
100
+
101
+ Dispatched actions are recorded, so they can be asserted on. `store.dispatch` is a jest mock:
102
+
103
+ ```js
104
+ expect(store.getActions()).toEqual([{ type: 'INCREMENT' }]);
105
+ expect(store.dispatch).toHaveBeenCalledWith({ type: 'INCREMENT' });
106
+
107
+ store.clearActions();
108
+ ```
109
+
110
+ __NOTE: `getActions()` reads the recorded calls of the `dispatch` mock, so a `jest.clearAllMocks()`
111
+ - e.g. within a `beforeEach` - also clears the recorded actions. Create the store after that call,
112
+ or use `store.clearActions()` to reset only this store.__
113
+
114
+ By default the state doesn't change when an action is dispatched, which keeps it predictable. Use
115
+ `setState` to simulate a state change - the subscribers are notified, so connected components
116
+ re-render:
117
+
118
+ ```js
119
+ store.setState({ counter: 5 });
120
+ store.setState(current => ({ counter: current.counter + 1 }));
121
+ ```
122
+
123
+ Pass a reducer as second argument to also apply dispatched actions to the state, e.g. to assert
124
+ what a component renders after it dispatched something:
125
+
126
+ ```js
127
+ const store = createMockStore({ counter: 0 }, (state, action) => (
128
+ action.type === 'INCREMENT' ? { counter: state.counter + 1 } : state
129
+ ));
130
+ ```
131
+
75
132
  ## About Shopgate
76
133
 
77
134
  Shopgate is the leading mobile commerce platform.
package/envSetup.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable max-len */
2
2
  /* eslint-disable require-jsdoc, extra-rules/potential-point-free, class-methods-use-this, no-unused-vars */
3
3
 
4
+ // Register React Testing Library's jest-dom matchers (toBeInTheDocument, toHaveTextContent, etc.)
4
5
  require('@testing-library/jest-dom');
5
6
 
6
7
  // Extend Jest matchers to add Enzyme specific ones (necessary since jest-enzyme packages was dropped)
@@ -9,70 +10,59 @@ expect.extend({
9
10
  const isFn = fn => typeof fn === 'function';
10
11
 
11
12
  // Prefer Enzyme's API when available (shallow)
12
- let empty =
13
- isFn(received.isEmptyRender) ? received.isEmptyRender() : null;
13
+ let empty = isFn(received.isEmptyRender) ? received.isEmptyRender() : null;
14
14
 
15
15
  // Fallback: try to infer from HTML (mount / generic wrappers)
16
16
  if (empty === null) {
17
17
  const html = isFn(received.html) ? received.html() : undefined;
18
18
  empty = html == null || /^\s*$/.test(String(html));
19
19
  }
20
-
21
20
  const pass = empty === true;
22
21
  return {
23
22
  pass,
24
- message: () =>
25
- (pass
26
- ? 'expected wrapper NOT to be an empty render'
27
- : 'expected wrapper to be an empty render'),
23
+ message: () => pass ? 'expected wrapper NOT to be an empty render' : 'expected wrapper to be an empty render'
28
24
  };
29
25
  },
30
26
  toExist(received) {
31
27
  const pass = typeof received.exists === 'function' ? received.exists() : !!received.length;
32
28
  return {
33
29
  pass,
34
- message: () =>
35
- (pass ? 'expected wrapper NOT to exist' : 'expected wrapper to exist'),
30
+ message: () => pass ? 'expected wrapper NOT to exist' : 'expected wrapper to exist'
36
31
  };
37
- },
32
+ }
38
33
  });
39
-
40
34
  global.SGEvent = {
41
- __call: () => { },
35
+ __call: () => {}
42
36
  };
43
-
44
37
  jest.setTimeout(10000);
45
-
46
38
  global.mutationConstructorSpy = jest.fn();
47
39
  global.mutationObserveSpy = jest.fn();
48
40
  global.mutationDisconnectSpy = jest.fn();
49
-
50
- global.MutationObserver = class {
51
- constructor(callback) { global.mutationConstructorSpy(callback); }
52
-
53
- observe(element, initObject) { global.mutationObserveSpy(element, initObject); }
54
-
55
- disconnect() { global.mutationDisconnectSpy(); }
56
- };
57
-
41
+ global.MutationObserver = /*#__PURE__*/function () {
42
+ function _class(callback) {
43
+ global.mutationConstructorSpy(callback);
44
+ }
45
+ var _proto = _class.prototype;
46
+ _proto.observe = function observe(element, initObject) {
47
+ global.mutationObserveSpy(element, initObject);
48
+ };
49
+ _proto.disconnect = function disconnect() {
50
+ global.mutationDisconnectSpy();
51
+ };
52
+ return _class;
53
+ }();
58
54
  global.IntersectionObserver = jest.fn((callback, options) => ({
59
- observe: jest.fn((element) => {
60
-
61
- }),
62
- unobserve: jest.fn((element) => {
63
-
64
- }),
65
- disconnect: jest.fn(() => {
66
-
67
- }),
55
+ observe: jest.fn(element => {}),
56
+ unobserve: jest.fn(element => {}),
57
+ disconnect: jest.fn(() => {}),
68
58
  takeRecords: jest.fn(() => []),
69
- trigger: (entries) => {
59
+ trigger: entries => {
70
60
  callback(entries, {
71
61
  disconnect: jest.fn(),
72
- unobserve: jest.fn(),
62
+ unobserve: jest.fn()
73
63
  });
74
- },
64
+ }
75
65
  }));
76
66
 
77
67
  /* eslint-enable require-jsdoc, extra-rules/potential-point-free, class-methods-use-this, no-unused-vars */
78
- /* eslint-enable max-len */
68
+ /* eslint-enable max-len */
package/jest.config.js CHANGED
@@ -1,3 +1,25 @@
1
+ // Resolve everything from this package so consumers need no babel-jest,
2
+ // preset install, or local babel config of their own.
3
+ const babelJest = require.resolve('babel-jest');
4
+ const babelOptions = {
5
+ babelrc: false,
6
+ configFile: false,
7
+ presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-react'), require.resolve('@babel/preset-typescript')]
8
+ };
9
+ const babelTransform = [babelJest, babelOptions];
10
+ const stylesMock = require.resolve("./mocks/styles.js");
11
+
12
+ /**
13
+ * Swiper exposes its React build via the "exports" field of its package.json, which jest can't
14
+ * resolve. Mapping it to the concrete file fixes that. Resolving it from this package works for
15
+ * consumers as well, and only falls back to the rootDir of the consumer when swiper is installed
16
+ * somewhere this package can't reach.
17
+ */
18
+ let swiperReact = '<rootDir>/node_modules/swiper/swiper-react.mjs';
19
+ try {
20
+ swiperReact = require.resolve('swiper/react');
21
+ } catch (e) {/* swiper is not installed */}
22
+
1
23
  /** @type {import('jest').Config} */
2
24
  module.exports = {
3
25
  testEnvironment: 'jsdom',
@@ -5,46 +27,30 @@ module.exports = {
5
27
  moduleFileExtensions: ['js', 'jsx', 'json', 'mjs', 'ts', 'tsx'],
6
28
  moduleNameMapper: {
7
29
  // Mock styles since they are not needed for unit tests
8
- '\\.(css|sass)$': '<rootDir>/__mocks__/styleMock.js',
9
- // Fix issue with Swiper ES module imports that work via "exports" field in package.json
10
- '^swiper/react$': '<rootDir>/node_modules/swiper/swiper-react.mjs',
30
+ '\\.(css|sass|scss|less)$': stylesMock,
31
+ // Mock assets since they are not needed for unit tests
32
+ '\\.(jpg|jpeg|png|gif|webp|svg|ico|eot|otf|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': require.resolve("./mocks/assets.js"),
33
+ '^swiper/react$': swiperReact,
34
+ // Mock Swiper styles - they are imported without a file extension
35
+ '^swiper/css(?:/.*)?$': stylesMock
11
36
  },
12
37
  transform: {
13
- '^.+\\.jsx?$': 'babel-jest',
14
- '^.+\\.mjs$': 'babel-jest',
15
- '^.+\\.[jt]sx?$': 'babel-jest',
38
+ '^.+\\.jsx?$': babelTransform,
39
+ '^.+\\.mjs$': babelTransform,
40
+ '^.+\\.[jt]sx?$': babelTransform
16
41
  },
17
- snapshotSerializers: [
18
- 'enzyme-to-json/serializer',
19
- ],
42
+ snapshotSerializers: ['enzyme-to-json/serializer'],
20
43
  testRegex: '(/__tests__/.*|(\\.|/)spec)\\.(js|jsx|ts|tsx)?$',
21
- testPathIgnorePatterns: [
22
- '/node_modules/',
23
- '/dist/',
24
- '/coverage/',
25
- '/config/',
26
- ],
27
- transformIgnorePatterns: [
28
- 'node_modules/(?!(swiper|dom7|intl-messageformat|@formatjs|tslib)/)',
29
- ],
30
- unmockedModulePathPatterns: [
31
- 'node_modules/react/',
32
- 'node_modules/enzyme/',
33
- ],
34
- collectCoverageFrom: [
35
- '**/*.{js,jsx,ts,tsx}',
36
- '!**/node_modules/**',
37
- '!**/coverage/**',
38
- '!.eslintrc.js',
39
- '!**/jest.config.js',
40
- ],
41
- setupFiles: [
42
- '@shopgate/pwa-unit-test/testSetup.js',
43
- ],
44
- setupFilesAfterEnv: [
45
- '@shopgate/pwa-unit-test/envSetup.js',
46
- ],
44
+ testPathIgnorePatterns: ['/node_modules/', '/dist/', '/coverage/', '/config/'],
45
+ // @shopgate packages are published untranspiled, so they need to be transformed as well. Within
46
+ // this repository they are symlinked to their sources outside of node_modules, which is why the
47
+ // missing entry didn't surface here, but only within extensions which install them for real.
48
+ transformIgnorePatterns: ['node_modules/(?!(@shopgate|swiper|dom7|intl-messageformat|@formatjs|tslib)/)'],
49
+ unmockedModulePathPatterns: ['node_modules/react/', 'node_modules/enzyme/'],
50
+ collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/node_modules/**', '!**/coverage/**', '!.eslintrc.js', '!**/jest.config.js'],
51
+ setupFiles: ['@shopgate/pwa-unit-test/testSetup.js'],
52
+ setupFilesAfterEnv: ['@shopgate/pwa-unit-test/envSetup.js'],
47
53
  testEnvironmentOptions: {
48
- url: 'http://localhost',
49
- },
50
- };
54
+ url: 'http://localhost'
55
+ }
56
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Mock for @shopgate/pwa-core/classes/AppCommand.
3
+ *
4
+ * The published packages don't contain their __mocks__ folders, so consumers like extensions can't
5
+ * rely on the manual mock of the package itself. Without a chainable mock the app commands that
6
+ * are dispatched on module level - e.g. by the Scanner class - throw as soon as a component from
7
+ * @shopgate/engage is imported.
8
+ */
9
+ let dispatchError = false;
10
+ const mockedSetCommandName = jest.fn(name => name);
11
+ const mockedSetCommandParams = jest.fn(params => params);
12
+ const mockedSetLibVersion = jest.fn(version => version);
13
+ const mockedBuildCommand = jest.fn(command => command);
14
+ const mockedDispatch = jest.fn(params => params);
15
+
16
+ /**
17
+ * Causes that the dispatch method resolves with FALSE.
18
+ * @param {boolean} value Trigger an error or not.
19
+ */
20
+ const triggerDispatchError = (value = true) => {
21
+ dispatchError = value;
22
+ };
23
+ const mockedAppCommand = jest.fn().mockImplementation(function MockedAppCommand() {
24
+ this.name = '';
25
+ this.params = null;
26
+ this.libVersion = '9.0';
27
+ this.setCommandName = name => {
28
+ this.name = mockedSetCommandName(name);
29
+ return this;
30
+ };
31
+ this.setCommandParams = params => {
32
+ this.params = mockedSetCommandParams(params);
33
+ return this;
34
+ };
35
+ this.setLibVersion = version => {
36
+ this.libVersion = mockedSetLibVersion(version);
37
+ return this;
38
+ };
39
+ this.buildCommand = () => {
40
+ const command = this.name ? {
41
+ c: this.name,
42
+ ...(this.params && {
43
+ p: this.params
44
+ })
45
+ } : null;
46
+ return mockedBuildCommand(command);
47
+ };
48
+ this.dispatch = params => {
49
+ mockedDispatch(params);
50
+ const success = !dispatchError;
51
+ dispatchError = false;
52
+ return Promise.resolve(success);
53
+ };
54
+ return this;
55
+ });
56
+ module.exports = {
57
+ __esModule: true,
58
+ default: mockedAppCommand,
59
+ mockedSetCommandName,
60
+ mockedSetCommandParams,
61
+ mockedSetLibVersion,
62
+ mockedBuildCommand,
63
+ mockedDispatch,
64
+ triggerDispatchError
65
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Replacement for asset imports (images, fonts, media) which are irrelevant for unit tests.
3
+ *
4
+ * Written in CommonJS on purpose, so that the mapping also works for consumers which replaced the
5
+ * transformIgnorePatterns of this package with ones that don't transform @shopgate packages.
6
+ */
7
+ module.exports = '';
package/mocks/i18n.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Mock for @shopgate/engage/core/helpers/i18n.
3
+ *
4
+ * The published packages don't contain their __mocks__ folders, so consumers like extensions can't
5
+ * rely on the manual mock of the package itself. Without it the translation helpers are not
6
+ * initialized, so components which render a translated string don't work.
7
+ */
8
+ const i18n = {
9
+ ready: true,
10
+ init: () => {},
11
+ text: input => input || '',
12
+ price: () => 'p',
13
+ number: () => 'n',
14
+ date: () => 'd',
15
+ time: () => 't',
16
+ getLang: () => 'de-DE',
17
+ getPath: path => path,
18
+ has: path => typeof path === 'string' && /^\S+\.\S+/.test(path)
19
+ };
20
+ module.exports = {
21
+ __esModule: true,
22
+ i18n,
23
+ getWeekDaysOrder: () => ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
24
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Replacement for style imports which are irrelevant for unit tests.
3
+ *
4
+ * Written in CommonJS on purpose, so that the mapping also works for consumers which replaced the
5
+ * transformIgnorePatterns of this package with ones that don't transform @shopgate packages.
6
+ */
7
+ module.exports = {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Mock for @shopgate/engage/styles/theme/hooks/useTheme.
3
+ *
4
+ * The published packages don't contain their __mocks__ folders, so consumers like extensions can't
5
+ * rely on the manual mock of the package itself. Without a theme object the styles of components
6
+ * from @shopgate/pwa-ui-shared throw when they access theme values.
7
+ *
8
+ * The theme is created with the published createTheme helper, so this mock doesn't need to
9
+ * duplicate any theme defaults.
10
+ */
11
+ const {
12
+ createTheme
13
+ } = require('@shopgate/engage/styles/theme/createTheme');
14
+ const theme = createTheme({});
15
+ module.exports = {
16
+ __esModule: true,
17
+ default: () => theme
18
+ };
package/package.json CHANGED
@@ -1,29 +1,34 @@
1
1
  {
2
2
  "name": "@shopgate/pwa-unit-test",
3
- "version": "7.32.0-beta.1",
3
+ "version": "7.32.0-beta.10",
4
4
  "description": "The unit tests setup for Shopgate's ENGAGE.",
5
5
  "author": "Shopgate <support@shopgate.com>",
6
6
  "license": "Apache-2.0",
7
7
  "dependencies": {
8
+ "@babel/core": "^7.29.7",
9
+ "@babel/preset-env": "^7.29.7",
10
+ "@babel/preset-react": "^7.29.7",
11
+ "@babel/preset-typescript": "^7.29.7",
8
12
  "@testing-library/dom": "^8.20.1",
9
13
  "@testing-library/jest-dom": "^5.16.5",
10
14
  "@testing-library/react": "^12.1.5",
11
15
  "@testing-library/user-event": "^13.5.0",
12
16
  "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
13
- "cheerio": "1.0.0-rc.2",
17
+ "babel-jest": "^29.7.0",
18
+ "cheerio": "1.0.0-rc.3",
14
19
  "enzyme": "^3.11.0",
15
- "enzyme-to-json": "^3.6.2"
20
+ "enzyme-to-json": "^3.6.2",
21
+ "jest-environment-jsdom": "^29.7.0"
16
22
  },
17
23
  "devDependencies": {
18
- "@babel/core": "^7.28.6",
19
- "@babel/preset-env": "^7.28.6",
20
- "@babel/preset-react": "^7.28.5",
21
24
  "@types/enzyme": "^3.10.19",
22
- "babel-jest": "^29.7.0",
25
+ "@types/jest": "^29.5.14",
26
+ "@types/react": "^17.0.83",
23
27
  "jest": "^29.7.0",
24
- "jest-environment-jsdom": "^29.7.0",
25
28
  "react": "^17.0.2",
26
- "react-dom": "^17.0.2"
29
+ "react-dom": "^17.0.2",
30
+ "react-redux": "^8.1.3",
31
+ "typescript": "^5.9.3"
27
32
  },
28
33
  "peerDependencies": {
29
34
  "jest": "^29.7.0",
package/testSetup.js CHANGED
@@ -1,13 +1,73 @@
1
1
  const Enzyme = require('enzyme');
2
2
  const Adapter = require('@wojtekmaj/enzyme-adapter-react-17');
3
+ Enzyme.configure({
4
+ adapter: new Adapter()
5
+ });
3
6
 
4
- Enzyme.configure({ adapter: new Adapter() });
7
+ /**
8
+ * Checks whether a module can be resolved. Consumers of this package don't necessarily have all
9
+ * of the mocked packages installed, and jest.mock() resolves the module path even when a factory
10
+ * is passed, so mocking an absent module would break the whole setup.
11
+ * @param {string} modulePath Path of the module.
12
+ * @returns {boolean} Whether the module can be resolved.
13
+ */
14
+ const canResolve = modulePath => {
15
+ try {
16
+ require.resolve(modulePath);
17
+ return true;
18
+ } catch (e) {
19
+ return false;
20
+ }
21
+ };
5
22
 
6
- jest.mock('@shopgate/engage/core/helpers/i18n');
23
+ /**
24
+ * The mocks below are provided by this package instead of relying on the manual __mocks__ folders
25
+ * of the mocked packages. Those folders are stripped from the published packages, so consumers
26
+ * like extensions would silently fall back to automocks, which breaks as soon as a component from
27
+ * @shopgate/engage is imported.
28
+ *
29
+ * The mock factories have to require their module lazily and inline, which is enforced by
30
+ * babel-plugin-jest-hoist.
31
+ */
32
+ /* eslint-disable global-require */
33
+
34
+ // Mock the translation helpers, since they are not initialized during test runs
35
+ if (canResolve('@shopgate/engage/core/helpers/i18n')) {
36
+ jest.mock('@shopgate/engage/core/helpers/i18n', () => require("./mocks/i18n"));
37
+ }
38
+
39
+ // Mock the AppCommand class by default to avoid log spamming during test runs
40
+ if (canResolve('@shopgate/pwa-core/classes/AppCommand')) {
41
+ jest.mock('@shopgate/pwa-core/classes/AppCommand', () => require("./mocks/appCommand"));
42
+ }
43
+
44
+ // Mock the useTheme hook to prevent failing tests due to missing theme object
45
+ if (canResolve('@shopgate/engage/styles/theme/hooks/useTheme')) {
46
+ jest.mock('@shopgate/engage/styles/theme/hooks/useTheme', () => require("./mocks/useTheme"));
47
+ }
48
+
49
+ // Mock the media provider styles to prevent failing tests
50
+ if (canResolve('@shopgate/pwa-common/collections/media-providers/style')) {
51
+ jest.mock('@shopgate/pwa-common/collections/media-providers/style', () => ({
52
+ responsiveContainer: 'responsiveContainer',
53
+ consentContainer: 'consentContainer',
54
+ consentLink: 'consentLink',
55
+ consentIcon: 'consentIcon'
56
+ }));
57
+ }
58
+
59
+ // The themes are not installed for consumers like extensions. They can't be mocked virtually,
60
+ // since a virtual mock is only applied to imports which use this exact module path.
61
+ if (canResolve('@shopgate/theme-gmd/extensions/reducers')) {
62
+ jest.mock('@shopgate/theme-gmd/extensions/reducers', () => null);
63
+ }
64
+ if (canResolve('@shopgate/theme-ios11/extensions/reducers')) {
65
+ jest.mock('@shopgate/theme-ios11/extensions/reducers', () => null);
66
+ }
67
+ /* eslint-enable global-require */
7
68
 
8
69
  const localStorageMock = (() => {
9
70
  let store = {};
10
-
11
71
  return {
12
72
  getItem(key) {
13
73
  return store[key] || null;
@@ -17,24 +77,28 @@ const localStorageMock = (() => {
17
77
  },
18
78
  clear() {
19
79
  store = {};
20
- },
80
+ }
21
81
  };
22
82
  })();
83
+ Object.defineProperty(window, 'localStorage', {
84
+ value: localStorageMock
85
+ });
23
86
 
24
- Object.defineProperty(window, 'localStorage', { value: localStorageMock });
25
-
26
- // Mock the AppCommand class by default to avoid log spamming during test runs
27
- jest.mock('@shopgate/pwa-core/classes/AppCommand');
28
- jest.mock('@shopgate/theme-gmd/extensions/reducers', () => null);
29
- jest.mock('@shopgate/theme-ios11/extensions/reducers', () => null);
30
-
31
- // Mock the media provider styles to prevent failing tests
32
- jest.mock('@shopgate/pwa-common/collections/media-providers/style', () => ({
33
- responsiveContainer: 'responsiveContainer',
34
- consentContainer: 'consentContainer',
35
- consentLink: 'consentLink',
36
- consentIcon: 'consentIcon',
37
- }));
38
-
39
- // Mock the useTheme hook to prevent failing tests due to missing theme object
40
- jest.mock('@shopgate/engage/styles/theme/hooks/useTheme');
87
+ // jsdom doesn't implement matchMedia. Components that check media queries - the `useReduceMotion`
88
+ // hook behind every v2 button, breakpoint providers - would throw in every spec that renders them.
89
+ // Specs that care about the result can still override this with their own stub.
90
+ if (!window.matchMedia) {
91
+ Object.defineProperty(window, 'matchMedia', {
92
+ writable: true,
93
+ value: query => ({
94
+ matches: false,
95
+ media: query,
96
+ onchange: null,
97
+ addListener: jest.fn(),
98
+ removeListener: jest.fn(),
99
+ addEventListener: jest.fn(),
100
+ removeEventListener: jest.fn(),
101
+ dispatchEvent: jest.fn()
102
+ })
103
+ });
104
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Minimal Redux store for component tests.
3
+ *
4
+ * This module is only meant to be imported from test files, since it uses the jest globals.
5
+ */
6
+ import type { Store } from 'redux';
7
+ /** An action which was dispatched to the store. */
8
+ export interface MockAction {
9
+ type: string;
10
+ [key: string]: unknown;
11
+ }
12
+ /** Reducer which is applied to the dispatched actions. */
13
+ export type MockReducer<S> = (state: S, action: MockAction) => S;
14
+ /**
15
+ * A store which satisfies the Store contract of redux, so that it can be passed to the Provider
16
+ * of react-redux without a cast, extended by the helpers which are useful within tests.
17
+ */
18
+ export interface MockStore<S> extends Store<S, MockAction> {
19
+ /** Records every dispatched action. Assert on it like on any other jest mock. */
20
+ dispatch: jest.Mock<MockAction, [MockAction]> & Store<S, MockAction>['dispatch'];
21
+ /** Replaces the state and notifies the subscribers, so connected components re-render. */
22
+ setState: (next: S | ((current: S) => S)) => void;
23
+ /** Returns the dispatched actions in the order they were dispatched. */
24
+ getActions: () => MockAction[];
25
+ /** Forgets all recorded actions. */
26
+ clearActions: () => void;
27
+ }
28
+ /**
29
+ * Creates a Redux store for component tests. It implements the contract that react-redux relies on
30
+ * - getState, dispatch and subscribe - without pulling in the reducers of the application.
31
+ *
32
+ * By default dispatched actions are only recorded, which keeps the state predictable while
33
+ * asserting that a component dispatches the right action. Pass a reducer to also apply the
34
+ * dispatched actions to the state, e.g. to assert what a component renders afterwards.
35
+ * @param initialState The state the store starts with.
36
+ * @param reducer Optional reducer which is applied to dispatched actions.
37
+ * @returns The mocked store.
38
+ */
39
+ export declare const createMockStore: <S>(initialState: S, reducer?: MockReducer<S>) => MockStore<S>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Minimal Redux store for component tests.
3
+ *
4
+ * This module is only meant to be imported from test files, since it uses the jest globals.
5
+ */
6
+
7
+ /**
8
+ * Symbol.observable is not part of the language, so redux resolves it with a string fallback. The
9
+ * same fallback is needed here - without it the computed property key would be undefined, and the
10
+ * store wouldn't fulfill the contract it claims to.
11
+ */
12
+ const $$observable = typeof Symbol === 'function' && Symbol.observable || '@@observable';
13
+
14
+ /** An action which was dispatched to the store. */
15
+
16
+ /** Reducer which is applied to the dispatched actions. */
17
+
18
+ /**
19
+ * A store which satisfies the Store contract of redux, so that it can be passed to the Provider
20
+ * of react-redux without a cast, extended by the helpers which are useful within tests.
21
+ */
22
+
23
+ /**
24
+ * Creates a Redux store for component tests. It implements the contract that react-redux relies on
25
+ * - getState, dispatch and subscribe - without pulling in the reducers of the application.
26
+ *
27
+ * By default dispatched actions are only recorded, which keeps the state predictable while
28
+ * asserting that a component dispatches the right action. Pass a reducer to also apply the
29
+ * dispatched actions to the state, e.g. to assert what a component renders afterwards.
30
+ * @param initialState The state the store starts with.
31
+ * @param reducer Optional reducer which is applied to dispatched actions.
32
+ * @returns The mocked store.
33
+ */
34
+ export const createMockStore = (initialState, reducer) => {
35
+ let state = initialState;
36
+ let currentReducer = reducer;
37
+ const listeners = new Set();
38
+
39
+ /**
40
+ * Notifies all subscribers about a state change.
41
+ */
42
+ const notify = () => {
43
+ listeners.forEach(listener => {
44
+ listener();
45
+ });
46
+ };
47
+ const dispatch = jest.fn(action => {
48
+ if (currentReducer) {
49
+ state = currentReducer(state, action);
50
+ notify();
51
+ }
52
+ return action;
53
+ });
54
+ const subscribe = listener => {
55
+ listeners.add(listener);
56
+ return () => {
57
+ listeners.delete(listener);
58
+ };
59
+ };
60
+ const getState = () => state;
61
+
62
+ /**
63
+ * Part of the redux Store contract. Tests don't observe the store, so this only satisfies the
64
+ * interface.
65
+ * @returns The observable of the store.
66
+ */
67
+ const observable = () => ({
68
+ subscribe: observer => {
69
+ /**
70
+ * Forwards the current state to the observer.
71
+ */
72
+ const handleChange = () => {
73
+ if (observer.next) {
74
+ observer.next(getState());
75
+ }
76
+ };
77
+ handleChange();
78
+ return {
79
+ unsubscribe: subscribe(handleChange)
80
+ };
81
+ },
82
+ [$$observable]() {
83
+ return this;
84
+ }
85
+ });
86
+
87
+ /**
88
+ * redux declares Symbol.observable as a plain symbol, so TypeScript widens the computed key to
89
+ * a symbol index and can't verify that this exact member is implemented. The assertion is only
90
+ * needed for that key - the store is verified against the contract in the spec.
91
+ */
92
+ return {
93
+ getState,
94
+ dispatch: dispatch,
95
+ subscribe,
96
+ replaceReducer: nextReducer => {
97
+ currentReducer = nextReducer;
98
+ notify();
99
+ },
100
+ [$$observable]: observable,
101
+ setState: next => {
102
+ state = typeof next === 'function' ? next(state) : next;
103
+ notify();
104
+ },
105
+ getActions: () => dispatch.mock.calls.map(([action]) => action),
106
+ clearActions: () => {
107
+ dispatch.mockClear();
108
+ }
109
+ };
110
+ };
@@ -0,0 +1 @@
1
+ export * from './createMockStore';
@@ -0,0 +1 @@
1
+ export * from "./createMockStore";