@khanacademy/wonder-blocks-testing 8.0.21 → 9.1.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 +17 -0
- package/dist/es/index.js +450 -33
- package/dist/harness/adapt.d.ts +17 -0
- package/dist/harness/adapter.d.ts +16 -0
- package/dist/harness/adapters/adapters.d.ts +1 -0
- package/dist/harness/adapters/ssr.d.ts +12 -0
- package/dist/harness/test-harness.d.ts +1 -0
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.js +449 -33
- package/dist/mock-requester.d.ts +2 -2
- package/package.json +2 -2
- package/src/fetch/fetch-request-matches-mock.ts +2 -4
- package/src/fetch/mock-fetch.ts +1 -1
- package/src/fixtures/__tests__/fixtures.test.tsx +9 -14
- package/src/fixtures/fixtures.basic.stories.tsx +9 -3
- package/src/fixtures/fixtures.tsx +1 -2
- package/src/gql/mock-gql-fetch.ts +1 -1
- package/src/harness/__tests__/adapt.test.tsx +200 -0
- package/src/harness/__tests__/adapter.test.tsx +67 -0
- package/src/harness/__tests__/hook-harness.test.ts +4 -2
- package/src/harness/__tests__/make-test-harness.test.tsx +16 -10
- package/src/harness/__tests__/test-harness.test.ts +4 -2
- package/src/harness/__tests__/types.typestest.tsx +6 -13
- package/src/harness/adapt.tsx +55 -0
- package/src/harness/adapter.tsx +27 -0
- package/src/harness/adapters/__tests__/data.test.tsx +12 -4
- package/src/harness/adapters/__tests__/ssr.test.tsx +82 -0
- package/src/harness/adapters/adapters.ts +3 -0
- package/src/harness/adapters/css.tsx +1 -3
- package/src/harness/adapters/router.tsx +5 -17
- package/src/harness/adapters/ssr.tsx +37 -0
- package/src/harness/{make-hook-harness.ts → make-hook-harness.tsx} +3 -2
- package/src/harness/make-test-harness.tsx +6 -8
- package/src/harness/types.ts +1 -1
- package/src/mock-requester.ts +4 -11
- package/src/respond-with.ts +2 -1
- package/src/settle-controller.ts +2 -3
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/harness/render-adapters.d.ts +0 -6
- package/src/harness/__tests__/render-adapters.test.tsx +0 -87
- package/src/harness/render-adapters.ts +0 -27
|
@@ -27,8 +27,7 @@ export const fixtures = <
|
|
|
27
27
|
let storyNumber = 1;
|
|
28
28
|
|
|
29
29
|
const getPropsOptions = {
|
|
30
|
-
|
|
31
|
-
log: (message, ...args) => action(message)(...args),
|
|
30
|
+
log: (message: string, ...args: Array<any>) => action(message)(...args),
|
|
32
31
|
logHandler: action,
|
|
33
32
|
} as const;
|
|
34
33
|
|
|
@@ -6,7 +6,7 @@ import type {GqlFetchMockFn, GqlMockOperation} from "./types";
|
|
|
6
6
|
* A mock for the fetch function passed to GqlRouter.
|
|
7
7
|
*/
|
|
8
8
|
export const mockGqlFetch = (): GqlFetchMockFn =>
|
|
9
|
-
mockRequester<GqlMockOperation<any, any, any
|
|
9
|
+
mockRequester<GqlMockOperation<any, any, any>>(
|
|
10
10
|
gqlRequestMatchesMock,
|
|
11
11
|
// Note that the identation at the start of each line is important.
|
|
12
12
|
// TODO(somewhatabstract): Make a stringify that indents each line of
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {render} from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import {Adapt} from "../adapt";
|
|
5
|
+
|
|
6
|
+
import type {TestHarnessAdapter, TestHarnessConfigs} from "../types";
|
|
7
|
+
|
|
8
|
+
describe("Adapt", () => {
|
|
9
|
+
it("should render children if no adapters", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const children = <div>Adapt me!</div>;
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
const {container: result} = render(
|
|
15
|
+
<Adapt adapters={{}} configs={{}}>
|
|
16
|
+
{children}
|
|
17
|
+
</Adapt>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(result).toMatchInlineSnapshot(`
|
|
22
|
+
<div>
|
|
23
|
+
<div>
|
|
24
|
+
Adapt me!
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should invoke the adapter with its corresponding config", () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const children = <div>Adapt me!</div>;
|
|
33
|
+
const adapters = {
|
|
34
|
+
adapterA: jest
|
|
35
|
+
.fn()
|
|
36
|
+
.mockReturnValue("ADAPTER A") as TestHarnessAdapter<string>,
|
|
37
|
+
} as const;
|
|
38
|
+
const configs: TestHarnessConfigs<typeof adapters> = {
|
|
39
|
+
adapterA: "APPLY A CONFIG",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
render(
|
|
44
|
+
<Adapt adapters={adapters} configs={configs}>
|
|
45
|
+
{children}
|
|
46
|
+
</Adapt>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(adapters.adapterA).toHaveBeenCalledWith(
|
|
51
|
+
expect.anything(),
|
|
52
|
+
"APPLY A CONFIG",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should render each adapter and the children", () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const children = "Adapt me!";
|
|
59
|
+
const adapter: TestHarnessAdapter<string> = (c: any, conf: any) => (
|
|
60
|
+
<>
|
|
61
|
+
{conf}
|
|
62
|
+
{c}
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
const adapters = {
|
|
66
|
+
adapterA: adapter,
|
|
67
|
+
adapterB: adapter,
|
|
68
|
+
adapterC: adapter,
|
|
69
|
+
} as const;
|
|
70
|
+
const configs: TestHarnessConfigs<typeof adapters> = {
|
|
71
|
+
adapterA: "A",
|
|
72
|
+
adapterB: "B",
|
|
73
|
+
adapterC: "C",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
const {container: result} = render(
|
|
78
|
+
<Adapt adapters={adapters} configs={configs}>
|
|
79
|
+
{children}
|
|
80
|
+
</Adapt>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(result).toMatchInlineSnapshot(`
|
|
85
|
+
<div>
|
|
86
|
+
C
|
|
87
|
+
B
|
|
88
|
+
A
|
|
89
|
+
Adapt me!
|
|
90
|
+
</div>
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should skip adapters where the corresponding config is null", () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const children = "Adapt me!";
|
|
97
|
+
const adapter: TestHarnessAdapter<string> = (c: any, conf: any) => (
|
|
98
|
+
<>
|
|
99
|
+
{conf}
|
|
100
|
+
{c}
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
const adapters = {
|
|
104
|
+
adapterA: adapter,
|
|
105
|
+
adapterB: adapter,
|
|
106
|
+
adapterC: adapter,
|
|
107
|
+
} as const;
|
|
108
|
+
const configs: TestHarnessConfigs<typeof adapters> = {
|
|
109
|
+
adapterA: "A",
|
|
110
|
+
adapterB: null,
|
|
111
|
+
adapterC: "C",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
const {container: result} = render(
|
|
116
|
+
<Adapt adapters={adapters} configs={configs}>
|
|
117
|
+
{children}
|
|
118
|
+
</Adapt>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(result).toMatchInlineSnapshot(`
|
|
123
|
+
<div>
|
|
124
|
+
C
|
|
125
|
+
A
|
|
126
|
+
Adapt me!
|
|
127
|
+
</div>
|
|
128
|
+
`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should render such that contexts are properly setup in order", () => {
|
|
132
|
+
// Arrange
|
|
133
|
+
const MyContext = React.createContext("root");
|
|
134
|
+
const ContextRoot = ({children}: any): React.ReactElement => {
|
|
135
|
+
const value = React.useContext(MyContext);
|
|
136
|
+
if (value !== "root") {
|
|
137
|
+
throw new Error("Don't nest this");
|
|
138
|
+
}
|
|
139
|
+
return (
|
|
140
|
+
<MyContext.Provider value={"other"}>
|
|
141
|
+
{children}
|
|
142
|
+
</MyContext.Provider>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
const adapterNoContext: TestHarnessAdapter<string> = (
|
|
146
|
+
c: any,
|
|
147
|
+
conf: any,
|
|
148
|
+
) => (
|
|
149
|
+
<>
|
|
150
|
+
{conf}
|
|
151
|
+
{c}
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
const adapterWithContext: TestHarnessAdapter<string> = (
|
|
155
|
+
c: any,
|
|
156
|
+
conf: string,
|
|
157
|
+
) => (
|
|
158
|
+
<ContextRoot>
|
|
159
|
+
{conf}
|
|
160
|
+
{c}
|
|
161
|
+
</ContextRoot>
|
|
162
|
+
);
|
|
163
|
+
const adapters = {
|
|
164
|
+
adapterA: adapterNoContext,
|
|
165
|
+
adapterB: adapterWithContext,
|
|
166
|
+
adapterC: adapterNoContext,
|
|
167
|
+
} as const;
|
|
168
|
+
const configs: TestHarnessConfigs<typeof adapters> = {
|
|
169
|
+
adapterA: "A",
|
|
170
|
+
adapterB: "B",
|
|
171
|
+
adapterC: "C",
|
|
172
|
+
};
|
|
173
|
+
const ChildComponent = (props: any): React.ReactElement => {
|
|
174
|
+
const value = React.useContext(MyContext);
|
|
175
|
+
if (value === "default") {
|
|
176
|
+
throw new Error("Context not setup properly");
|
|
177
|
+
}
|
|
178
|
+
return <div>{value}</div>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Act
|
|
182
|
+
const {container: result} = render(
|
|
183
|
+
<Adapt adapters={adapters} configs={configs}>
|
|
184
|
+
<ChildComponent />
|
|
185
|
+
</Adapt>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Assert
|
|
189
|
+
expect(result).toMatchInlineSnapshot(`
|
|
190
|
+
<div>
|
|
191
|
+
C
|
|
192
|
+
B
|
|
193
|
+
A
|
|
194
|
+
<div>
|
|
195
|
+
other
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
`);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {render} from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import {TestHarnessAdapter} from "../types";
|
|
5
|
+
|
|
6
|
+
import {Adapter} from "../adapter";
|
|
7
|
+
|
|
8
|
+
describe("Adapter", () => {
|
|
9
|
+
it("should render only the children if the adapter config is nullish", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const adapter: TestHarnessAdapter<string> = (children, config) => (
|
|
12
|
+
<div id="adapter">
|
|
13
|
+
{config}
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
const children = "Adapt me!";
|
|
18
|
+
|
|
19
|
+
// Act
|
|
20
|
+
const {container: result} = render(
|
|
21
|
+
<Adapter
|
|
22
|
+
adapter={adapter}
|
|
23
|
+
config={null as null | undefined | string}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</Adapter>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(result).toMatchInlineSnapshot(`
|
|
31
|
+
<div>
|
|
32
|
+
Adapt me!
|
|
33
|
+
</div>
|
|
34
|
+
`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should render the adapter around the children if the config is not nullish", () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const adapter: TestHarnessAdapter<string> = (children, config) => (
|
|
40
|
+
<div id="adapter">
|
|
41
|
+
{config}
|
|
42
|
+
{children}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
const children = "Adapt me!";
|
|
46
|
+
const config = "this-is-the-config";
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const {container: result} = render(
|
|
50
|
+
<Adapter adapter={adapter} config={config}>
|
|
51
|
+
{children}
|
|
52
|
+
</Adapter>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(result).toMatchInlineSnapshot(`
|
|
57
|
+
<div>
|
|
58
|
+
<div
|
|
59
|
+
id="adapter"
|
|
60
|
+
>
|
|
61
|
+
this-is-the-config
|
|
62
|
+
Adapt me!
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -34,7 +34,8 @@ describe("#hookHarness", () => {
|
|
|
34
34
|
const config = {
|
|
35
35
|
router: "/boo",
|
|
36
36
|
} as const;
|
|
37
|
-
// @ts-expect-error
|
|
37
|
+
// @ts-expect-error We know harnessFake isn't real, we add it in the
|
|
38
|
+
// mocks at the top of this file.
|
|
38
39
|
const [{harnessFake}, {hookHarness}] = await ws.isolateModules(() =>
|
|
39
40
|
Promise.all([
|
|
40
41
|
import("../make-hook-harness"),
|
|
@@ -54,7 +55,8 @@ describe("#hookHarness", () => {
|
|
|
54
55
|
const config = {
|
|
55
56
|
router: "/boo",
|
|
56
57
|
} as const;
|
|
57
|
-
// @ts-expect-error
|
|
58
|
+
// @ts-expect-error We know harnessFake isn't real, we add it in the
|
|
59
|
+
// mocks at the top of this file.
|
|
58
60
|
const [{returnValueFake}, {hookHarness}] = await ws.isolateModules(() =>
|
|
59
61
|
Promise.all([
|
|
60
62
|
import("../make-hook-harness"),
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import {Route} from "react-router-dom";
|
|
3
3
|
import {render} from "@testing-library/react";
|
|
4
4
|
|
|
5
|
-
import * as RA from "../
|
|
5
|
+
import * as RA from "../adapt";
|
|
6
6
|
import {makeTestHarness} from "../make-test-harness";
|
|
7
7
|
import {DefaultConfigs, DefaultAdapters} from "../adapters/adapters";
|
|
8
8
|
|
|
@@ -75,7 +75,7 @@ describe("#makeTestHarness", () => {
|
|
|
75
75
|
DefaultConfigs,
|
|
76
76
|
);
|
|
77
77
|
const Component = () => <div>test</div>;
|
|
78
|
-
const renderSpy = jest.spyOn(RA, "
|
|
78
|
+
const renderSpy = jest.spyOn(RA, "Adapt");
|
|
79
79
|
|
|
80
80
|
// Act
|
|
81
81
|
const HarnessedComponent = testHarness(Component);
|
|
@@ -83,9 +83,12 @@ describe("#makeTestHarness", () => {
|
|
|
83
83
|
|
|
84
84
|
// Assert
|
|
85
85
|
expect(renderSpy).toHaveBeenCalledWith(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
{
|
|
87
|
+
adapters: DefaultAdapters,
|
|
88
|
+
configs: DefaultConfigs,
|
|
89
|
+
children: expect.anything(),
|
|
90
|
+
},
|
|
91
|
+
{},
|
|
89
92
|
);
|
|
90
93
|
});
|
|
91
94
|
|
|
@@ -132,7 +135,7 @@ describe("#makeTestHarness", () => {
|
|
|
132
135
|
router: "/mysecretplace",
|
|
133
136
|
};
|
|
134
137
|
const Component = () => <div>test</div>;
|
|
135
|
-
const renderSpy = jest.spyOn(RA, "
|
|
138
|
+
const renderSpy = jest.spyOn(RA, "Adapt");
|
|
136
139
|
|
|
137
140
|
// Act
|
|
138
141
|
const HarnessedComponent = testHarness(
|
|
@@ -143,12 +146,15 @@ describe("#makeTestHarness", () => {
|
|
|
143
146
|
|
|
144
147
|
// Assert
|
|
145
148
|
expect(renderSpy).toHaveBeenCalledWith(
|
|
146
|
-
DefaultAdapters,
|
|
147
149
|
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
adapters: DefaultAdapters,
|
|
151
|
+
configs: {
|
|
152
|
+
...DefaultConfigs,
|
|
153
|
+
router: configOverrides.router,
|
|
154
|
+
},
|
|
155
|
+
children: expect.anything(),
|
|
150
156
|
},
|
|
151
|
-
|
|
157
|
+
{},
|
|
152
158
|
);
|
|
153
159
|
});
|
|
154
160
|
|
|
@@ -35,7 +35,8 @@ describe("#testHarness", () => {
|
|
|
35
35
|
const config = {
|
|
36
36
|
router: "/boo",
|
|
37
37
|
} as const;
|
|
38
|
-
// @ts-expect-error
|
|
38
|
+
// @ts-expect-error We know harnessFake isn't real, we add it in the
|
|
39
|
+
// mocks at the top of this file.
|
|
39
40
|
const [{harnessFake}, {testHarness}] = await ws.isolateModules(() =>
|
|
40
41
|
Promise.all([
|
|
41
42
|
import("../make-test-harness"),
|
|
@@ -56,7 +57,8 @@ describe("#testHarness", () => {
|
|
|
56
57
|
const config = {
|
|
57
58
|
router: "/boo",
|
|
58
59
|
} as const;
|
|
59
|
-
// @ts-expect-error
|
|
60
|
+
// @ts-expect-error We know harnessFake isn't real, we add it in the
|
|
61
|
+
// mocks at the top of this file.
|
|
60
62
|
const [{returnValueFake}, {testHarness}] = await ws.isolateModules(() =>
|
|
61
63
|
Promise.all([
|
|
62
64
|
import("../make-test-harness"),
|
|
@@ -12,13 +12,10 @@ import type {
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
//> should assert type of config.
|
|
15
|
-
// @ts-expect-error
|
|
16
|
-
((
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// $FlowExpectedError[incompatible-cast]
|
|
20
|
-
config: number,
|
|
21
|
-
): React.ReactElement<any> => <div />) as TestHarnessAdapter<string>;
|
|
15
|
+
// @ts-expect-error TConfig is string, but we typed config as a number
|
|
16
|
+
((children: React.ReactNode, config: number): React.ReactElement<any> => (
|
|
17
|
+
<div />
|
|
18
|
+
)) as TestHarnessAdapter<string>;
|
|
22
19
|
//<
|
|
23
20
|
|
|
24
21
|
//> should work for correct definition
|
|
@@ -36,10 +33,8 @@ import type {
|
|
|
36
33
|
//<
|
|
37
34
|
|
|
38
35
|
//> should assert if adapter is not Adapter<TConfig>
|
|
39
|
-
// @ts-expect-error
|
|
36
|
+
// @ts-expect-error String is not a adapter function
|
|
40
37
|
({
|
|
41
|
-
// String is not a adapter function
|
|
42
|
-
// $FlowExpectedError[incompatible-cast]
|
|
43
38
|
adapterString: "string",
|
|
44
39
|
} as TestHarnessAdapters);
|
|
45
40
|
//<
|
|
@@ -100,11 +95,9 @@ const adapters = {
|
|
|
100
95
|
//<
|
|
101
96
|
|
|
102
97
|
//> should assert if config does not match adapter config
|
|
103
|
-
// @ts-expect-error:
|
|
98
|
+
// @ts-expect-error: the config type here is a number, not a string
|
|
104
99
|
({
|
|
105
100
|
adapterA: "a string, this is correct",
|
|
106
|
-
// the config type here is a number, not a string
|
|
107
|
-
// $FlowExpectedError[incompatible-cast]
|
|
108
101
|
adapterB: "a string, but it should be a number",
|
|
109
102
|
} as TestHarnessConfigs<typeof adapters>);
|
|
110
103
|
//<
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import {Adapter} from "./adapter";
|
|
4
|
+
|
|
5
|
+
import type {TestHarnessConfigs, TestHarnessAdapters} from "./types";
|
|
6
|
+
|
|
7
|
+
type Props<TAdapters extends TestHarnessAdapters> = {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
adapters: TAdapters;
|
|
10
|
+
configs: TestHarnessConfigs<TAdapters>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a set of adapters around the given children.
|
|
15
|
+
*
|
|
16
|
+
* Adapters are rendered with the last adapter being the outermost and the first
|
|
17
|
+
* adapter being the innermost, with children being the innermost of all. This
|
|
18
|
+
* ensures that we are backwards compatible with previous releases of the
|
|
19
|
+
* test harness.
|
|
20
|
+
*/
|
|
21
|
+
export const Adapt = <TAdapters extends TestHarnessAdapters>({
|
|
22
|
+
children,
|
|
23
|
+
adapters,
|
|
24
|
+
configs,
|
|
25
|
+
}: Props<TAdapters>): React.ReactElement => {
|
|
26
|
+
// We start at the end of the adapter list and work backwards to be
|
|
27
|
+
// compatible with previous releases.
|
|
28
|
+
const thisAdapterName = Object.keys(adapters).at(-1);
|
|
29
|
+
if (thisAdapterName == null) {
|
|
30
|
+
// There are no adapters. Just render the children.
|
|
31
|
+
return <>{children}</>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const thisAdapter = adapters[thisAdapterName];
|
|
35
|
+
const thisConfig = configs[thisAdapterName];
|
|
36
|
+
|
|
37
|
+
// NOTE: We could simplify this by using an array of tuples as the input
|
|
38
|
+
// prop, but that complicates other things like testing and I think
|
|
39
|
+
// for simplicities sake elsewhere, this is good enough.
|
|
40
|
+
const restAdapters = Object.fromEntries(
|
|
41
|
+
Object.entries(adapters).slice(0, -1),
|
|
42
|
+
);
|
|
43
|
+
const restConfigs = Object.fromEntries(
|
|
44
|
+
// The config object may not be ordered the same as the adapters so
|
|
45
|
+
// we must filter by adapter name here.
|
|
46
|
+
Object.entries(configs).filter(([name]) => name !== thisAdapterName),
|
|
47
|
+
);
|
|
48
|
+
return (
|
|
49
|
+
<Adapter adapter={thisAdapter} config={thisConfig}>
|
|
50
|
+
<Adapt adapters={restAdapters} configs={restConfigs}>
|
|
51
|
+
{children}
|
|
52
|
+
</Adapt>
|
|
53
|
+
</Adapter>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type {TestHarnessAdapter} from "./types";
|
|
4
|
+
|
|
5
|
+
type Props<TConfig, TAdapter extends TestHarnessAdapter<TConfig>> = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
adapter: TAdapter;
|
|
8
|
+
config: TConfig | null | undefined;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Component that optionally renders a given adapter with the given config.
|
|
13
|
+
*
|
|
14
|
+
* If the config is nullish, then the children are rendered in a fragment,
|
|
15
|
+
* otherwise the children are rendered within the given adapter with the
|
|
16
|
+
* given config.
|
|
17
|
+
*/
|
|
18
|
+
export const Adapter = <TConfig, TAdapter extends TestHarnessAdapter<TConfig>>({
|
|
19
|
+
children,
|
|
20
|
+
adapter,
|
|
21
|
+
config,
|
|
22
|
+
}: Props<TConfig, TAdapter>): React.ReactElement => {
|
|
23
|
+
if (config == null) {
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
|
26
|
+
return adapter(children, config);
|
|
27
|
+
};
|
|
@@ -21,8 +21,12 @@ describe("WonderBlocksData.adapter", () => {
|
|
|
21
21
|
const TestFixture = () => {
|
|
22
22
|
const [result] = useCachedEffect("ID", jest.fn());
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
CONTENT:{" "}
|
|
27
|
+
{result.status === "success" ? result.data : undefined}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
// Act
|
|
@@ -43,8 +47,12 @@ describe("WonderBlocksData.adapter", () => {
|
|
|
43
47
|
const TestFixture = () => {
|
|
44
48
|
const [result] = useCachedEffect("ID", jest.fn());
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
CONTENT:
|
|
53
|
+
{result.status === "success" ? result.data : undefined}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
48
56
|
};
|
|
49
57
|
|
|
50
58
|
// Act
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {render, screen} from "@testing-library/react";
|
|
3
|
+
import * as WBCore from "@khanacademy/wonder-blocks-core";
|
|
4
|
+
import {makeTestHarness} from "../../make-test-harness";
|
|
5
|
+
|
|
6
|
+
import * as SSR from "../ssr";
|
|
7
|
+
|
|
8
|
+
jest.mock("@khanacademy/wonder-stuff-core", () => {
|
|
9
|
+
const actualCore = jest.requireActual("@khanacademy/wonder-stuff-core");
|
|
10
|
+
return {
|
|
11
|
+
...actualCore,
|
|
12
|
+
RenderStateRoot: (props: any) => (
|
|
13
|
+
<actualCore.RenderStateRoot {...props} />
|
|
14
|
+
),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("SSR.adapter", () => {
|
|
19
|
+
it("should render the RenderStateRoot", () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const children = <div>CHILDREN!</div>;
|
|
22
|
+
const renderStateRootSpy = jest.spyOn(WBCore, "RenderStateRoot");
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
render(SSR.adapter(children, true));
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(renderStateRootSpy).toHaveBeenCalledWith(
|
|
29
|
+
{
|
|
30
|
+
children,
|
|
31
|
+
},
|
|
32
|
+
{},
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should render the children correctly", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const children = <div>CHILDREN!</div>;
|
|
39
|
+
|
|
40
|
+
// Act
|
|
41
|
+
render(SSR.adapter(children, true));
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
expect(screen.getByText("CHILDREN!")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should enable harnessing of components that require RenderStateRoot", () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const ComponentNeedsSsr = (props: any) => {
|
|
50
|
+
const idf = WBCore.useUniqueIdWithoutMock();
|
|
51
|
+
return <div>{idf?.get("my-id")}</div>;
|
|
52
|
+
};
|
|
53
|
+
const testHarness = makeTestHarness(
|
|
54
|
+
{
|
|
55
|
+
ssr: SSR.adapter,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
ssr: true,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
const Harnessed = testHarness(ComponentNeedsSsr);
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
const underTest = () => render(<Harnessed />);
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
expect(underTest).not.toThrowError();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should throw on bad configuration", () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
const children = <div>CHILDREN!</div>;
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
const underTest = () => render(SSR.adapter(children, false as any));
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
79
|
+
`"Unexpected configuration: set config to null to turn this adapter off"`,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -2,6 +2,7 @@ import * as css from "./css";
|
|
|
2
2
|
import * as data from "./data";
|
|
3
3
|
import * as portal from "./portal";
|
|
4
4
|
import * as router from "./router";
|
|
5
|
+
import * as ssr from "./ssr";
|
|
5
6
|
|
|
6
7
|
import type {TestHarnessConfigs} from "../types";
|
|
7
8
|
|
|
@@ -19,6 +20,7 @@ export const DefaultAdapters = {
|
|
|
19
20
|
data: data.adapter,
|
|
20
21
|
portal: portal.adapter,
|
|
21
22
|
router: router.adapter,
|
|
23
|
+
ssr: ssr.adapter,
|
|
22
24
|
} as const;
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -29,4 +31,5 @@ export const DefaultConfigs: TestHarnessConfigs<typeof DefaultAdapters> = {
|
|
|
29
31
|
data: data.defaultConfig,
|
|
30
32
|
portal: portal.defaultConfig,
|
|
31
33
|
router: router.defaultConfig,
|
|
34
|
+
ssr: ssr.defaultConfig,
|
|
32
35
|
} as const;
|
|
@@ -40,9 +40,7 @@ const normalizeConfig = (
|
|
|
40
40
|
return config;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
// that `config` can be.
|
|
45
|
-
return {classes: [], style: config};
|
|
43
|
+
return {classes: [], style: config as CSSProperties};
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
throw new Error(`Invalid config: ${config}`);
|