@shopgate/pwa-unit-test 7.31.5 → 7.31.6-beta.1

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
@@ -10,70 +10,59 @@ expect.extend({
10
10
  const isFn = fn => typeof fn === 'function';
11
11
 
12
12
  // Prefer Enzyme's API when available (shallow)
13
- let empty =
14
- isFn(received.isEmptyRender) ? received.isEmptyRender() : null;
13
+ let empty = isFn(received.isEmptyRender) ? received.isEmptyRender() : null;
15
14
 
16
15
  // Fallback: try to infer from HTML (mount / generic wrappers)
17
16
  if (empty === null) {
18
17
  const html = isFn(received.html) ? received.html() : undefined;
19
18
  empty = html == null || /^\s*$/.test(String(html));
20
19
  }
21
-
22
20
  const pass = empty === true;
23
21
  return {
24
22
  pass,
25
- message: () =>
26
- (pass
27
- ? 'expected wrapper NOT to be an empty render'
28
- : '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'
29
24
  };
30
25
  },
31
26
  toExist(received) {
32
27
  const pass = typeof received.exists === 'function' ? received.exists() : !!received.length;
33
28
  return {
34
29
  pass,
35
- message: () =>
36
- (pass ? 'expected wrapper NOT to exist' : 'expected wrapper to exist'),
30
+ message: () => pass ? 'expected wrapper NOT to exist' : 'expected wrapper to exist'
37
31
  };
38
- },
32
+ }
39
33
  });
40
-
41
34
  global.SGEvent = {
42
- __call: () => { },
35
+ __call: () => {}
43
36
  };
44
-
45
37
  jest.setTimeout(10000);
46
-
47
38
  global.mutationConstructorSpy = jest.fn();
48
39
  global.mutationObserveSpy = jest.fn();
49
40
  global.mutationDisconnectSpy = jest.fn();
50
-
51
- global.MutationObserver = class {
52
- constructor(callback) { global.mutationConstructorSpy(callback); }
53
-
54
- observe(element, initObject) { global.mutationObserveSpy(element, initObject); }
55
-
56
- disconnect() { global.mutationDisconnectSpy(); }
57
- };
58
-
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
+ }();
59
54
  global.IntersectionObserver = jest.fn((callback, options) => ({
60
- observe: jest.fn((element) => {
61
-
62
- }),
63
- unobserve: jest.fn((element) => {
64
-
65
- }),
66
- disconnect: jest.fn(() => {
67
-
68
- }),
55
+ observe: jest.fn(element => {}),
56
+ unobserve: jest.fn(element => {}),
57
+ disconnect: jest.fn(() => {}),
69
58
  takeRecords: jest.fn(() => []),
70
- trigger: (entries) => {
59
+ trigger: entries => {
71
60
  callback(entries, {
72
61
  disconnect: jest.fn(),
73
- unobserve: jest.fn(),
62
+ unobserve: jest.fn()
74
63
  });
75
- },
64
+ }
76
65
  }));
77
66
 
78
67
  /* eslint-enable require-jsdoc, extra-rules/potential-point-free, class-methods-use-this, no-unused-vars */
79
- /* eslint-enable max-len */
68
+ /* eslint-enable max-len */
package/jest.config.js CHANGED
@@ -4,13 +4,21 @@ const babelJest = require.resolve('babel-jest');
4
4
  const babelOptions = {
5
5
  babelrc: false,
6
6
  configFile: false,
7
- presets: [
8
- require.resolve('@babel/preset-env'),
9
- require.resolve('@babel/preset-react'),
10
- require.resolve('@babel/preset-typescript'),
11
- ],
7
+ presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-react'), require.resolve('@babel/preset-typescript')]
12
8
  };
13
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 */}
14
22
 
15
23
  /** @type {import('jest').Config} */
16
24
  module.exports = {
@@ -19,46 +27,30 @@ module.exports = {
19
27
  moduleFileExtensions: ['js', 'jsx', 'json', 'mjs', 'ts', 'tsx'],
20
28
  moduleNameMapper: {
21
29
  // Mock styles since they are not needed for unit tests
22
- '\\.(css|sass)$': '<rootDir>/__mocks__/styleMock.js',
23
- // Fix issue with Swiper ES module imports that work via "exports" field in package.json
24
- '^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
25
36
  },
26
37
  transform: {
27
38
  '^.+\\.jsx?$': babelTransform,
28
39
  '^.+\\.mjs$': babelTransform,
29
- '^.+\\.[jt]sx?$': babelTransform,
40
+ '^.+\\.[jt]sx?$': babelTransform
30
41
  },
31
- snapshotSerializers: [
32
- 'enzyme-to-json/serializer',
33
- ],
42
+ snapshotSerializers: ['enzyme-to-json/serializer'],
34
43
  testRegex: '(/__tests__/.*|(\\.|/)spec)\\.(js|jsx|ts|tsx)?$',
35
- testPathIgnorePatterns: [
36
- '/node_modules/',
37
- '/dist/',
38
- '/coverage/',
39
- '/config/',
40
- ],
41
- transformIgnorePatterns: [
42
- 'node_modules/(?!(swiper|dom7|intl-messageformat|@formatjs|tslib)/)',
43
- ],
44
- unmockedModulePathPatterns: [
45
- 'node_modules/react/',
46
- 'node_modules/enzyme/',
47
- ],
48
- collectCoverageFrom: [
49
- '**/*.{js,jsx,ts,tsx}',
50
- '!**/node_modules/**',
51
- '!**/coverage/**',
52
- '!.eslintrc.js',
53
- '!**/jest.config.js',
54
- ],
55
- setupFiles: [
56
- '@shopgate/pwa-unit-test/testSetup.js',
57
- ],
58
- setupFilesAfterEnv: [
59
- '@shopgate/pwa-unit-test/envSetup.js',
60
- ],
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'],
61
53
  testEnvironmentOptions: {
62
- url: 'http://localhost',
63
- },
64
- };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopgate/pwa-unit-test",
3
- "version": "7.31.5",
3
+ "version": "7.31.6-beta.1",
4
4
  "description": "The unit tests setup for Shopgate's ENGAGE.",
5
5
  "author": "Shopgate <support@shopgate.com>",
6
6
  "license": "Apache-2.0",
@@ -22,9 +22,13 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/enzyme": "^3.10.19",
25
+ "@types/jest": "^29.5.14",
26
+ "@types/react": "^17.0.83",
25
27
  "jest": "^29.7.0",
26
28
  "react": "^17.0.2",
27
- "react-dom": "^17.0.2"
29
+ "react-dom": "^17.0.2",
30
+ "react-redux": "^8.1.3",
31
+ "typescript": "^5.9.3"
28
32
  },
29
33
  "peerDependencies": {
30
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,9 @@ const localStorageMock = (() => {
17
77
  },
18
78
  clear() {
19
79
  store = {};
20
- },
80
+ }
21
81
  };
22
82
  })();
23
-
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');
83
+ Object.defineProperty(window, 'localStorage', {
84
+ value: localStorageMock
85
+ });
@@ -0,0 +1,40 @@
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
+ type Listener = () => void;
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
+ export interface MockStore<S> {
15
+ /** Returns the current state. */
16
+ getState: () => S;
17
+ /** Records every dispatched action. Assert on it like on any other jest mock. */
18
+ dispatch: jest.Mock<MockAction, [MockAction]>;
19
+ /** Registers a listener which is invoked whenever the state changed. Returns an unsubscribe. */
20
+ subscribe: (listener: Listener) => () => void;
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>;
40
+ export {};
@@ -0,0 +1,59 @@
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
+ /** An action which was dispatched to the store. */
8
+
9
+ /** Reducer which is applied to the dispatched actions. */
10
+
11
+ /**
12
+ * Creates a Redux store for component tests. It implements the contract that react-redux relies on
13
+ * - getState, dispatch and subscribe - without pulling in the reducers of the application.
14
+ *
15
+ * By default dispatched actions are only recorded, which keeps the state predictable while
16
+ * asserting that a component dispatches the right action. Pass a reducer to also apply the
17
+ * dispatched actions to the state, e.g. to assert what a component renders afterwards.
18
+ * @param initialState The state the store starts with.
19
+ * @param reducer Optional reducer which is applied to dispatched actions.
20
+ * @returns The mocked store.
21
+ */
22
+ export const createMockStore = (initialState, reducer) => {
23
+ let state = initialState;
24
+ const listeners = new Set();
25
+
26
+ /**
27
+ * Notifies all subscribers about a state change.
28
+ */
29
+ const notify = () => {
30
+ listeners.forEach(listener => {
31
+ listener();
32
+ });
33
+ };
34
+ const dispatch = jest.fn(action => {
35
+ if (reducer) {
36
+ state = reducer(state, action);
37
+ notify();
38
+ }
39
+ return action;
40
+ });
41
+ return {
42
+ getState: () => state,
43
+ dispatch,
44
+ subscribe: listener => {
45
+ listeners.add(listener);
46
+ return () => {
47
+ listeners.delete(listener);
48
+ };
49
+ },
50
+ setState: next => {
51
+ state = typeof next === 'function' ? next(state) : next;
52
+ notify();
53
+ },
54
+ getActions: () => dispatch.mock.calls.map(([action]) => action),
55
+ clearActions: () => {
56
+ dispatch.mockClear();
57
+ }
58
+ };
59
+ };
@@ -0,0 +1 @@
1
+ export * from './createMockStore';
@@ -0,0 +1 @@
1
+ export * from "./createMockStore";