@khanacademy/wonder-blocks-testing 4.0.4 → 5.0.2

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/es/index.js +212 -26
  3. package/dist/index.js +589 -62
  4. package/package.json +5 -3
  5. package/src/__docs__/_overview_.stories.mdx +3 -4
  6. package/src/__docs__/_overview_fixtures.stories.mdx +22 -0
  7. package/src/__docs__/_overview_mocking.stories.mdx +14 -0
  8. package/src/__docs__/_overview_test_harness.stories.mdx +18 -0
  9. package/src/__docs__/exports.fixture-adapters.stories.mdx +49 -0
  10. package/src/__docs__/exports.fixtures.stories.mdx +53 -0
  11. package/src/__docs__/exports.harness-adapters.stories.mdx +187 -0
  12. package/src/__docs__/exports.hook-harness.stories.mdx +22 -0
  13. package/src/__docs__/exports.make-hook-harness.stories.mdx +25 -0
  14. package/src/__docs__/exports.make-test-harness.stories.mdx +28 -0
  15. package/src/__docs__/exports.mock-fetch.stories.mdx +40 -0
  16. package/src/__docs__/exports.mock-gql-fetch.stories.mdx +13 -8
  17. package/src/__docs__/exports.respond-with.stories.mdx +54 -8
  18. package/src/__docs__/exports.setup-fixtures.stories.mdx +22 -0
  19. package/src/__docs__/exports.test-harness.stories.mdx +23 -0
  20. package/src/__docs__/types.custom-mount-props.stories.mdx +35 -0
  21. package/src/__docs__/types.fetch-mock-fn.stories.mdx +22 -0
  22. package/src/__docs__/types.fetch-mock-operation.stories.mdx +18 -0
  23. package/src/__docs__/types.fixtures-adapter-factory.stories.mdx +23 -0
  24. package/src/__docs__/types.fixtures-adapter-fixture-options.stories.mdx +35 -0
  25. package/src/__docs__/types.fixtures-adapter-group-options.stories.mdx +37 -0
  26. package/src/__docs__/types.fixtures-adapter-group.stories.mdx +43 -0
  27. package/src/__docs__/types.fixtures-adapter-options.stories.mdx +21 -0
  28. package/src/__docs__/types.fixtures-adapter.stories.mdx +35 -0
  29. package/src/__docs__/types.fixtures-configuration.stories.mdx +35 -0
  30. package/src/__docs__/types.fixtures-options.stories.mdx +51 -0
  31. package/src/__docs__/types.get-props-options.stories.mdx +25 -0
  32. package/src/__docs__/types.gql-fetch-mock-fn.stories.mdx +27 -0
  33. package/src/__docs__/types.gql-mock-operation.stories.mdx +26 -0
  34. package/src/__docs__/types.mock-response.stories.mdx +18 -0
  35. package/src/__docs__/types.test-harness-adapter.stories.mdx +21 -0
  36. package/src/__docs__/types.test-harness-adapters.stories.mdx +46 -0
  37. package/src/__docs__/types.test-harness-config.stories.mdx +18 -0
  38. package/src/__docs__/types.test-harness-configs.stories.mdx +59 -0
  39. package/src/fetch/types.js +0 -3
  40. package/src/fixtures/adapters/adapter-group.js +11 -11
  41. package/src/fixtures/adapters/adapter.js +8 -8
  42. package/src/fixtures/adapters/storybook.js +11 -8
  43. package/src/fixtures/fixtures.basic.stories.js +6 -2
  44. package/src/fixtures/fixtures.defaultwrapper.stories.js +6 -2
  45. package/src/fixtures/setup.js +8 -4
  46. package/src/fixtures/types.js +27 -16
  47. package/src/gql/types.js +1 -3
  48. package/src/harness/__tests__/hook-harness.test.js +72 -0
  49. package/src/harness/__tests__/make-hook-harness.test.js +94 -0
  50. package/src/harness/__tests__/make-test-harness.test.js +190 -0
  51. package/src/harness/__tests__/render-adapters.test.js +88 -0
  52. package/src/harness/__tests__/test-harness.test.js +74 -0
  53. package/src/harness/__tests__/types.flowtest.js +115 -0
  54. package/src/harness/adapters/__tests__/__snapshots__/router.test.js.snap +5 -0
  55. package/src/harness/adapters/__tests__/css.test.js +96 -0
  56. package/src/harness/adapters/__tests__/data.test.js +66 -0
  57. package/src/harness/adapters/__tests__/portal.test.js +31 -0
  58. package/src/harness/adapters/__tests__/router.test.js +233 -0
  59. package/src/harness/adapters/adapters.js +33 -0
  60. package/src/harness/adapters/css.js +65 -0
  61. package/src/harness/adapters/data.js +46 -0
  62. package/src/harness/adapters/portal.js +26 -0
  63. package/src/harness/adapters/router.js +206 -0
  64. package/src/harness/hook-harness.js +23 -0
  65. package/src/harness/make-hook-harness.js +39 -0
  66. package/src/harness/make-test-harness.js +68 -0
  67. package/src/harness/render-adapters.js +27 -0
  68. package/src/harness/test-harness.js +24 -0
  69. package/src/harness/types.js +57 -0
  70. package/src/index.js +22 -18
@@ -0,0 +1,233 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {withRouter, Prompt} from "react-router-dom";
4
+ import {render} from "@testing-library/react";
5
+ import * as Router from "../router.js";
6
+
7
+ describe("Router.adapter", () => {
8
+ it("should throw if the config does not match any expecations", () => {
9
+ // Arrange
10
+ const badConfig: any = {
11
+ bad: "config",
12
+ };
13
+
14
+ // Act
15
+ const underTest = () => Router.adapter("CHILDREN", badConfig);
16
+
17
+ // Assert
18
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
19
+ `"A location or initial history entries must be provided."`,
20
+ );
21
+ });
22
+
23
+ describe.each`
24
+ type | config
25
+ ${"string"} | ${"/math"}
26
+ ${"location"} | ${{location: "/math"}}
27
+ ${"full"} | ${{initialEntries: ["/math"]}}
28
+ `("with $type config", ({config}) => {
29
+ it("should allow navigation", () => {
30
+ // Arrange
31
+ const historyListen = jest.fn();
32
+ const HistoryListener = withRouter(({history}): React.Node => {
33
+ React.useEffect(() => history.listen(historyListen), [history]);
34
+ if (history.location.pathname === "/math") {
35
+ history.push("/math/calculator");
36
+ }
37
+ return null;
38
+ });
39
+
40
+ // Act
41
+ render(Router.adapter(<HistoryListener />, config));
42
+
43
+ // Assert
44
+ expect(historyListen).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it("should have default route match of root /", () => {
48
+ // Arrange
49
+ const matchCatcherFn = jest.fn();
50
+ const MatchCatcher = withRouter(({match, history}): React.Node => {
51
+ React.useEffect(() => {
52
+ if (history.location.pathname === "/math") {
53
+ history.push("/math/calculator");
54
+ }
55
+ matchCatcherFn(match);
56
+ }, [match, history]);
57
+ return null;
58
+ });
59
+
60
+ // Act
61
+ render(Router.adapter(<MatchCatcher />, config));
62
+
63
+ // Assert
64
+ expect(matchCatcherFn).toHaveBeenLastCalledWith(
65
+ expect.objectContaining({
66
+ path: "/",
67
+ url: "/",
68
+ }),
69
+ );
70
+ });
71
+ });
72
+
73
+ describe.each`
74
+ type | config
75
+ ${"location"} | ${{location: "/math/calculator", path: "/math/*"}}
76
+ ${"full"} | ${{initialEntries: ["/math/calculator"], path: "/math/*"}}
77
+ `("with $type config including path", ({config}) => {
78
+ it("should include routing for the given path", () => {
79
+ // Arrange
80
+ const matchCatcherFn = jest.fn();
81
+ const MatchCatcher = withRouter(({match}): React.Node => {
82
+ React.useEffect(() => {
83
+ matchCatcherFn(match);
84
+ }, [match]);
85
+ return null;
86
+ });
87
+
88
+ // Act
89
+ render(Router.adapter(<MatchCatcher />, config));
90
+
91
+ // Assert
92
+ expect(matchCatcherFn).toHaveBeenLastCalledWith(
93
+ expect.objectContaining({
94
+ isExact: true,
95
+ path: "/math/*",
96
+ url: "/math/calculator",
97
+ }),
98
+ );
99
+ });
100
+
101
+ it("should throw if the path does not match the location", () => {
102
+ // Arrange
103
+ // This is going to cause an error to be logged, so let's silence
104
+ // that.
105
+ jest.spyOn(console, "error").mockImplementation(() => {});
106
+ const badConfig = {
107
+ ...config,
108
+ path: "/something/else/entirely",
109
+ };
110
+
111
+ // Act
112
+ const underTest = () =>
113
+ render(Router.adapter("CHILDREN", badConfig));
114
+
115
+ // Assert
116
+ expect(underTest).toThrowErrorMatchingSnapshot();
117
+ });
118
+ });
119
+
120
+ describe("with forceStatic", () => {
121
+ it("should not navigate", () => {
122
+ // Arrange
123
+ const historyListen = jest.fn();
124
+ const HistoryListener = withRouter(({history}): React.Node => {
125
+ React.useEffect(() => history.listen(historyListen), [history]);
126
+ if (history.location.pathname === "/math") {
127
+ history.push("/math/calculator");
128
+ }
129
+ return null;
130
+ });
131
+
132
+ // Act
133
+ render(
134
+ Router.adapter(<HistoryListener />, {
135
+ location: "/math",
136
+ forceStatic: true,
137
+ }),
138
+ );
139
+
140
+ // Assert
141
+ expect(historyListen).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+
145
+ describe("with initialEntries", () => {
146
+ it("should use the defaultConfig location if initialEntries is empty", () => {
147
+ // Arrange
148
+ const matchCatcherFn = jest.fn();
149
+ const MatchCatcher = withRouter(({match, history}): React.Node => {
150
+ React.useEffect(() => {
151
+ matchCatcherFn(match);
152
+ }, [match, history]);
153
+ return null;
154
+ });
155
+
156
+ // Act
157
+ render(
158
+ Router.adapter(<MatchCatcher />, {
159
+ initialEntries: [],
160
+ }),
161
+ );
162
+
163
+ // Assert
164
+ expect(matchCatcherFn).toHaveBeenLastCalledWith(
165
+ expect.objectContaining({
166
+ url: Router.defaultConfig.location,
167
+ }),
168
+ );
169
+ });
170
+
171
+ it("should set initialIndex prop on MemoryRouter if given in configuration", () => {
172
+ // Arrange
173
+ const matchCatcherFn = jest.fn();
174
+ const MatchCatcher = withRouter(({match}): React.Node => {
175
+ React.useEffect(() => {
176
+ matchCatcherFn(match);
177
+ }, [match]);
178
+ return null;
179
+ });
180
+
181
+ // Act
182
+ render(
183
+ Router.adapter(<MatchCatcher />, {
184
+ initialEntries: ["/location/old", "/location/current"],
185
+ initialIndex: 1,
186
+ path: "/location/*",
187
+ }),
188
+ );
189
+
190
+ // Assert
191
+ expect(matchCatcherFn).toHaveBeenLastCalledWith(
192
+ expect.objectContaining({
193
+ url: "/location/current",
194
+ }),
195
+ );
196
+ });
197
+
198
+ it("should set getUserConfirmation prop on MemoryRouter if given in configuration", () => {
199
+ // Arrange
200
+ const getUserConfirmationSpy = jest
201
+ .fn()
202
+ .mockImplementation((message, cb) => {
203
+ cb(true);
204
+ });
205
+ const matchCatcherFn = jest.fn();
206
+ const MatchCatcher = withRouter(({match, history}): React.Node => {
207
+ React.useEffect(() => {
208
+ if (history.location.pathname === "/location/old") {
209
+ // Fire off a location change.
210
+ history.goForward();
211
+ }
212
+ matchCatcherFn(match);
213
+ }, [match, history]);
214
+ return <Prompt message="Are you sure?" />;
215
+ });
216
+
217
+ // Act
218
+ render(
219
+ Router.adapter(<MatchCatcher />, {
220
+ initialEntries: ["/location/old", "/location/current"],
221
+ getUserConfirmation: getUserConfirmationSpy,
222
+ path: "/location/*",
223
+ }),
224
+ );
225
+
226
+ // Assert
227
+ expect(getUserConfirmationSpy).toHaveBeenCalledWith(
228
+ "Are you sure?",
229
+ expect.any(Function),
230
+ );
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,33 @@
1
+ // @flow
2
+ import * as css from "./css.js";
3
+ import * as data from "./data.js";
4
+ import * as portal from "./portal.js";
5
+ import * as router from "./router.js";
6
+
7
+ import type {TestHarnessConfigs} from "../types.js";
8
+
9
+ /**
10
+ * NOTE: We do not type `DefaultAdapters` with `Adapters` here because we want
11
+ * the individual config types of each adapter to remain intact rather than
12
+ * getting changed to `any`.
13
+ */
14
+
15
+ /**
16
+ * The default adapters provided by Wonder Blocks.
17
+ */
18
+ export const DefaultAdapters = {
19
+ css: css.adapter,
20
+ data: data.adapter,
21
+ portal: portal.adapter,
22
+ router: router.adapter,
23
+ };
24
+
25
+ /**
26
+ * The default configurations to use with the `DefaultAdapters`.
27
+ */
28
+ export const DefaultConfigs: TestHarnessConfigs<typeof DefaultAdapters> = {
29
+ css: css.defaultConfig,
30
+ data: data.defaultConfig,
31
+ portal: portal.defaultConfig,
32
+ router: router.defaultConfig,
33
+ };
@@ -0,0 +1,65 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import type {CSSProperties} from "aphrodite";
5
+
6
+ import type {TestHarnessAdapter} from "../types.js";
7
+
8
+ type Config =
9
+ | string
10
+ | Array<string>
11
+ | CSSProperties
12
+ | {|
13
+ classes: Array<string>,
14
+ style: CSSProperties,
15
+ |};
16
+
17
+ // The default configuration is to omit this adapter.
18
+ export const defaultConfig: ?Config = null;
19
+
20
+ const normalizeConfig = (
21
+ config: Config,
22
+ ): {|classes: Array<string>, style: CSSProperties|} => {
23
+ if (typeof config === "string") {
24
+ return {classes: [config], style: ({}: $Shape<CSSProperties>)};
25
+ }
26
+
27
+ if (Array.isArray(config)) {
28
+ return {classes: config, style: ({}: $Shape<CSSProperties>)};
29
+ }
30
+
31
+ if (typeof config === "object") {
32
+ if (config.classes != null && config.style != null) {
33
+ // This is a heuristic check and by nature isn't perfect.
34
+ // So we have to tell flow to just accept it.
35
+ // $FlowIgnore[prop-missing]
36
+ return config;
37
+ }
38
+
39
+ // Again, since the previous check is heuristic, so is this outcome
40
+ // and so we still have to assure flow everything is OK.
41
+ // $FlowIgnore[prop-missing]
42
+ return {classes: [], style: config};
43
+ }
44
+
45
+ throw new Error(`Invalid config: ${config}`);
46
+ };
47
+
48
+ /**
49
+ * Test harness adapter for adding CSS to the harnessed component wrapper.
50
+ */
51
+ export const adapter: TestHarnessAdapter<Config> = (
52
+ children: React.Node,
53
+ config: Config,
54
+ ): React.Element<any> => {
55
+ const {classes, style} = normalizeConfig(config);
56
+ return (
57
+ <div
58
+ data-test-id="css-adapter-container"
59
+ className={classes.join(" ")}
60
+ style={style}
61
+ >
62
+ {children}
63
+ </div>
64
+ );
65
+ };
@@ -0,0 +1,46 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {InterceptRequests} from "@khanacademy/wonder-blocks-data";
4
+ import type {TestHarnessAdapter} from "../types.js";
5
+
6
+ type Interceptor = React.ElementConfig<typeof InterceptRequests>["interceptor"];
7
+
8
+ type Config = Interceptor | Array<Interceptor>;
9
+
10
+ /**
11
+ * Default configuration for the Wonder Blocks Data adapter.
12
+ */
13
+ export const defaultConfig = ([]: Array<Interceptor>);
14
+
15
+ /**
16
+ * Test harness adapter to mock Wonder Blocks Data usage.
17
+ *
18
+ * NOTE: Consumers are responsible for properly defining their intercepts.
19
+ * This component does not validate the configuration to ensure interceptors
20
+ * are not overriding one another.
21
+ */
22
+ export const adapter: TestHarnessAdapter<Config> = (
23
+ children: React.Node,
24
+ config: Config,
25
+ ): React.Element<any> => {
26
+ // First we render the cache intercepts.
27
+ let currentChildren = children;
28
+
29
+ const interceptors = Array.isArray(config) ? config : [config];
30
+
31
+ // Then we render the data intercepts.
32
+ for (const interceptor of interceptors) {
33
+ currentChildren = (
34
+ <InterceptRequests interceptor={interceptor}>
35
+ {currentChildren}
36
+ </InterceptRequests>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * `currentChildren` is a `React.Node` but we need it to be a
42
+ * `React.Element<>`. Return it rendered in a fragment allows us to do
43
+ * that.
44
+ */
45
+ return <>{currentChildren}</>;
46
+ };
@@ -0,0 +1,26 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import type {TestHarnessAdapter} from "../types.js";
5
+
6
+ type Config = string;
7
+
8
+ // The default configuration is to omit this adapter.
9
+ export const defaultConfig: ?Config = null;
10
+
11
+ /**
12
+ * Test harness adapter for supporting portals.
13
+ *
14
+ * Some components rely on rendering with a React Portal. This adapter ensures
15
+ * that the DOM contains a mounting point for the portal with the expected
16
+ * identifier.
17
+ */
18
+ export const adapter: TestHarnessAdapter<Config> = (
19
+ children: React.Node,
20
+ config: Config,
21
+ ): React.Element<any> => (
22
+ <>
23
+ <div id={config} data-test-id={config} />
24
+ {children}
25
+ </>
26
+ );
@@ -0,0 +1,206 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {StaticRouter, MemoryRouter, Route, Switch} from "react-router-dom";
5
+
6
+ import type {LocationShape, Location} from "react-router-dom";
7
+ import type {TestHarnessAdapter} from "../types.js";
8
+
9
+ type MemoryRouterProps = React.ElementConfig<typeof MemoryRouter>;
10
+
11
+ /**
12
+ * Configuration for the withLocation test harness adapter.
13
+ */
14
+ type Config =
15
+ | $ReadOnly<
16
+ | {|
17
+ /**
18
+ * See MemoryRouter prop for initialEntries.
19
+ */
20
+ initialEntries: MemoryRouterProps["initialEntries"],
21
+
22
+ /**
23
+ * See MemoryRouter prop for initialIndex.
24
+ */
25
+ initialIndex?: MemoryRouterProps["initialIndex"],
26
+
27
+ /**
28
+ * See MemoryRouter prop for getUserConfirmation.
29
+ */
30
+ getUserConfirmation?: MemoryRouterProps["getUserConfirmation"],
31
+
32
+ /**
33
+ * A path match to use.
34
+ *
35
+ * When this is specified, the harnessed component will be
36
+ * rendered inside a `Route` handler with this path.
37
+ *
38
+ * If the path matches the location, then the route will
39
+ * render the component.
40
+ *
41
+ * If the path does not match the location, then the route
42
+ * will not render the component.
43
+ */
44
+ path?: string,
45
+ |}
46
+ | {|
47
+ /**
48
+ * The location to use.
49
+ */
50
+ location: string | Location,
51
+
52
+ /**
53
+ * Force the use of a StaticRouter, instead of MemoryRouter.
54
+ */
55
+ forceStatic: true,
56
+
57
+ /**
58
+ * A path match to use.
59
+ *
60
+ * When this is specified, the harnessed component will be
61
+ * rendered inside a `Route` handler with this path.
62
+ *
63
+ * If the path matches the location, then the route will
64
+ * render the component.
65
+ *
66
+ * If the path does not match the location, then the route
67
+ * will not render the component.
68
+ */
69
+ path?: string,
70
+ |}
71
+ | {|
72
+ /**
73
+ * The initial location to use.
74
+ */
75
+ location: string | LocationShape,
76
+
77
+ /**
78
+ * A path match to use.
79
+ *
80
+ * When this is specified, the harnessed component will be
81
+ * rendered inside a `Route` handler with this path.
82
+ *
83
+ * If the path matches the location, then the route will
84
+ * render the component.
85
+ *
86
+ * If the path does not match the location, then the route
87
+ * will not render the component.
88
+ */
89
+ path?: string,
90
+ |},
91
+ >
92
+ // The initial location to use.
93
+ | string;
94
+
95
+ /**
96
+ * The default configuration for this adapter.
97
+ */
98
+ export const defaultConfig = {location: "/"};
99
+
100
+ const maybeWithRoute = (children: React.Node, path: ?string): React.Node => {
101
+ if (path == null) {
102
+ return children;
103
+ }
104
+
105
+ return (
106
+ <Switch>
107
+ <Route exact={true} path={path}>
108
+ {children}
109
+ </Route>
110
+ <Route
111
+ path="*"
112
+ render={() => {
113
+ throw new Error(
114
+ "The configured path must match the configured location or your harnessed component will not render.",
115
+ );
116
+ }}
117
+ />
118
+ </Switch>
119
+ );
120
+ };
121
+
122
+ /**
123
+ * Adapter that sets up a router and AppShell location-specific contexts.
124
+ *
125
+ * This allows you to ensure that components are being tested in the
126
+ * AppShell world.
127
+ *
128
+ * NOTE(somewhatabstract): The AppShell component itself already does
129
+ * the work of setting up routing and the AppShellContext and so using this
130
+ * adapter with the App component will have zero-effect since AppShell will
131
+ * override it.
132
+ */
133
+ export const adapter: TestHarnessAdapter<Config> = (
134
+ children: React.Node,
135
+ config: Config,
136
+ ): React.Element<any> => {
137
+ if (typeof config === "string") {
138
+ config = {
139
+ location: config,
140
+ };
141
+ }
142
+
143
+ // Wrap children with the various contexts and routes, as per the config.
144
+ const wrappedWithRoute = maybeWithRoute(children, config.path);
145
+ if (config.forceStatic) {
146
+ /**
147
+ * There may be times (SSR testing comes to mind) where we will be
148
+ * really strict about not permitting client-side navigation events.
149
+ */
150
+ return (
151
+ <StaticRouter location={config.location} context={{}}>
152
+ {wrappedWithRoute}
153
+ </StaticRouter>
154
+ );
155
+ }
156
+
157
+ /**
158
+ * OK, we must be OK with a memory router.
159
+ *
160
+ * There are two flavors of config for this. The easy one with just a
161
+ * location, and the complex one for those gnarlier setups.
162
+ *
163
+ * First, the easy one.
164
+ */
165
+ if (typeof config.location !== "undefined") {
166
+ return (
167
+ <MemoryRouter initialEntries={[config.location]}>
168
+ {wrappedWithRoute}
169
+ </MemoryRouter>
170
+ );
171
+ }
172
+
173
+ /**
174
+ * If it's not the easy one, it should be the complex one.
175
+ * Let's make sure we have good data (also keeps flow happy).
176
+ */
177
+ if (typeof config.initialEntries === "undefined") {
178
+ throw new Error(
179
+ "A location or initial history entries must be provided.",
180
+ );
181
+ }
182
+
183
+ /**
184
+ * What should happen if no entries were in the array?
185
+ * It likely uses the root one anyway, but a consistent API is what
186
+ * we want, so let's ensure we always have our default location at least.
187
+ */
188
+ const entries =
189
+ config.initialEntries.length === 0
190
+ ? [defaultConfig.location]
191
+ : config.initialEntries;
192
+
193
+ // Memory router doesn't allow us to pass maybe types in its flow types.
194
+ // So let's build props then spread them.
195
+ const routerProps: MemoryRouterProps = {
196
+ initialEntries: entries,
197
+ };
198
+ if (config.initialIndex != null) {
199
+ routerProps.initialIndex = config.initialIndex;
200
+ }
201
+ if (config.getUserConfirmation != null) {
202
+ routerProps.getUserConfirmation = config.getUserConfirmation;
203
+ }
204
+
205
+ return <MemoryRouter {...routerProps}>{wrappedWithRoute}</MemoryRouter>;
206
+ };
@@ -0,0 +1,23 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {makeHookHarness} from "./make-hook-harness.js";
5
+ import {DefaultAdapters, DefaultConfigs} from "./adapters/adapters.js";
6
+
7
+ import type {TestHarnessConfigs} from "./types.js";
8
+
9
+ /**
10
+ * Create test wrapper for hook testing with Wonder Blocks default adapters.
11
+ *
12
+ * This is primarily useful for tests within Wonder Blocks.
13
+ *
14
+ * If you want to expand the range of adapters or change the default
15
+ * configurations, use `makeHookHarness` to create a new `hookHarness`
16
+ * function.
17
+ */
18
+ export const hookHarness: (
19
+ configs?: $Shape<TestHarnessConfigs<typeof DefaultAdapters>>,
20
+ ) => React.AbstractComponent<any, any> = makeHookHarness(
21
+ DefaultAdapters,
22
+ DefaultConfigs,
23
+ );
@@ -0,0 +1,39 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {makeTestHarness} from "./make-test-harness.js";
5
+
6
+ import type {TestHarnessAdapters, TestHarnessConfigs} from "./types.js";
7
+
8
+ const HookHarness = ({children}) => children;
9
+
10
+ /**
11
+ * Create a test harness method for use with React hooks.
12
+ *
13
+ * This returns a test harness method that applies the default configurations
14
+ * to the given adapters, wrapping a given component.
15
+ *
16
+ * @param {TAdapters} adapters All the adapters to be supported by the returned
17
+ * test harness.
18
+ * @param {TestHarnessConfigs<TAdapters>} defaultConfigs Default configuration values for
19
+ * the adapters.
20
+ * @returns {(
21
+ * configs?: $Shape<TestHarnessConfigs<TAdapters>>,
22
+ * ) => React.AbstractComponent<any, any>} A test harness.
23
+ */
24
+ export const makeHookHarness = <TAdapters: TestHarnessAdapters>(
25
+ adapters: TAdapters,
26
+ defaultConfigs: TestHarnessConfigs<TAdapters>,
27
+ ): ((
28
+ configs?: $Shape<TestHarnessConfigs<TAdapters>>,
29
+ ) => React.AbstractComponent<any, any>) => {
30
+ const testHarness = makeTestHarness<TAdapters>(adapters, defaultConfigs);
31
+ /**
32
+ * Create a harness to use as a wrapper when rendering hooks.
33
+ *
34
+ * @param {$Shape<Configs<typeof DefaultAdapters>>} [configs] Any adapter
35
+ * configuration that you want to override from the DefaultConfigs values.
36
+ */
37
+ return (configs?: $Shape<TestHarnessConfigs<TAdapters>>) =>
38
+ testHarness<any, any>(HookHarness, configs);
39
+ };