@khanacademy/wonder-blocks-testing 4.0.4 → 5.0.0
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/CHANGELOG.md +7 -0
- package/dist/es/index.js +212 -26
- package/dist/index.js +589 -62
- package/package.json +4 -2
- package/src/__docs__/_overview_.stories.mdx +3 -4
- package/src/__docs__/_overview_fixtures.stories.mdx +22 -0
- package/src/__docs__/_overview_mocking.stories.mdx +14 -0
- package/src/__docs__/_overview_test_harness.stories.mdx +18 -0
- package/src/__docs__/exports.fixture-adapters.stories.mdx +49 -0
- package/src/__docs__/exports.fixtures.stories.mdx +53 -0
- package/src/__docs__/exports.harness-adapters.stories.mdx +187 -0
- package/src/__docs__/exports.hook-harness.stories.mdx +22 -0
- package/src/__docs__/exports.make-hook-harness.stories.mdx +25 -0
- package/src/__docs__/exports.make-test-harness.stories.mdx +28 -0
- package/src/__docs__/exports.mock-fetch.stories.mdx +40 -0
- package/src/__docs__/exports.mock-gql-fetch.stories.mdx +13 -8
- package/src/__docs__/exports.respond-with.stories.mdx +54 -8
- package/src/__docs__/exports.setup-fixtures.stories.mdx +22 -0
- package/src/__docs__/exports.test-harness.stories.mdx +23 -0
- package/src/__docs__/types.custom-mount-props.stories.mdx +35 -0
- package/src/__docs__/types.fetch-mock-fn.stories.mdx +22 -0
- package/src/__docs__/types.fetch-mock-operation.stories.mdx +18 -0
- package/src/__docs__/types.fixtures-adapter-factory.stories.mdx +23 -0
- package/src/__docs__/types.fixtures-adapter-fixture-options.stories.mdx +35 -0
- package/src/__docs__/types.fixtures-adapter-group-options.stories.mdx +37 -0
- package/src/__docs__/types.fixtures-adapter-group.stories.mdx +43 -0
- package/src/__docs__/types.fixtures-adapter-options.stories.mdx +21 -0
- package/src/__docs__/types.fixtures-adapter.stories.mdx +35 -0
- package/src/__docs__/types.fixtures-configuration.stories.mdx +35 -0
- package/src/__docs__/types.fixtures-options.stories.mdx +51 -0
- package/src/__docs__/types.get-props-options.stories.mdx +25 -0
- package/src/__docs__/types.gql-fetch-mock-fn.stories.mdx +27 -0
- package/src/__docs__/types.gql-mock-operation.stories.mdx +26 -0
- package/src/__docs__/types.mock-response.stories.mdx +18 -0
- package/src/__docs__/types.test-harness-adapter.stories.mdx +21 -0
- package/src/__docs__/types.test-harness-adapters.stories.mdx +46 -0
- package/src/__docs__/types.test-harness-config.stories.mdx +18 -0
- package/src/__docs__/types.test-harness-configs.stories.mdx +59 -0
- package/src/fetch/types.js +0 -3
- package/src/fixtures/adapters/adapter-group.js +11 -11
- package/src/fixtures/adapters/adapter.js +8 -8
- package/src/fixtures/adapters/storybook.js +11 -8
- package/src/fixtures/fixtures.basic.stories.js +6 -2
- package/src/fixtures/fixtures.defaultwrapper.stories.js +6 -2
- package/src/fixtures/setup.js +8 -4
- package/src/fixtures/types.js +27 -16
- package/src/gql/types.js +1 -3
- package/src/harness/__tests__/hook-harness.test.js +72 -0
- package/src/harness/__tests__/make-hook-harness.test.js +94 -0
- package/src/harness/__tests__/make-test-harness.test.js +190 -0
- package/src/harness/__tests__/render-adapters.test.js +88 -0
- package/src/harness/__tests__/test-harness.test.js +74 -0
- package/src/harness/adapters/__tests__/__snapshots__/router.test.js.snap +5 -0
- package/src/harness/adapters/__tests__/css.test.js +96 -0
- package/src/harness/adapters/__tests__/data.test.js +66 -0
- package/src/harness/adapters/__tests__/portal.test.js +31 -0
- package/src/harness/adapters/__tests__/router.test.js +233 -0
- package/src/harness/adapters/adapters.js +33 -0
- package/src/harness/adapters/css.js +65 -0
- package/src/harness/adapters/data.js +46 -0
- package/src/harness/adapters/portal.js +26 -0
- package/src/harness/adapters/router.js +206 -0
- package/src/harness/hook-harness.js +23 -0
- package/src/harness/make-hook-harness.js +39 -0
- package/src/harness/make-test-harness.js +68 -0
- package/src/harness/render-adapters.js +27 -0
- package/src/harness/test-harness.js +24 -0
- package/src/harness/types.js +57 -0
- 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
|
+
};
|