@khanacademy/wonder-blocks-testing 2.0.8 → 4.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 +22 -0
- package/dist/es/index.js +255 -140
- package/dist/index.js +304 -145
- package/package.json +2 -1
- package/src/{gql/__tests__/make-gql-mock-response.test.js → __tests__/make-mock-response.test.js} +196 -34
- package/src/__tests__/mock-requester.test.js +213 -0
- package/src/__tests__/response-impl.test.js +47 -0
- package/src/fetch/__tests__/__snapshots__/mock-fetch.test.js.snap +29 -0
- package/src/fetch/__tests__/fetch-request-matches-mock.test.js +99 -0
- package/src/fetch/__tests__/mock-fetch.test.js +84 -0
- package/src/fetch/fetch-request-matches-mock.js +43 -0
- package/src/fetch/mock-fetch.js +19 -0
- package/src/fetch/types.js +18 -0
- package/src/fixtures/__tests__/fixtures.test.js +66 -22
- package/src/fixtures/adapters/__tests__/adapter-group.test.js +24 -0
- package/src/fixtures/adapters/__tests__/adapter.test.js +6 -0
- package/src/fixtures/adapters/storybook.js +4 -1
- package/src/fixtures/fixtures.basic.stories.js +1 -1
- package/src/fixtures/fixtures.defaultwrapper.stories.js +1 -1
- package/src/fixtures/fixtures.js +47 -3
- package/src/fixtures/types.js +10 -1
- package/src/gql/__tests__/mock-gql-fetch.test.js +24 -15
- package/src/gql/__tests__/wb-data-integration.test.js +7 -4
- package/src/gql/mock-gql-fetch.js +9 -80
- package/src/gql/types.js +11 -10
- package/src/index.js +9 -3
- package/src/make-mock-response.js +150 -0
- package/src/mock-requester.js +75 -0
- package/src/response-impl.js +9 -0
- package/src/types.js +39 -0
- package/src/gql/make-gql-mock-response.js +0 -124
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Request} from "node-fetch";
|
|
3
|
+
import {fetchRequestMatchesMock} from "../fetch-request-matches-mock.js";
|
|
4
|
+
|
|
5
|
+
const TEST_URL = "http://example.com/foo?querya=1&queryb=elephants#fragment";
|
|
6
|
+
|
|
7
|
+
describe("#fetchRequestMatchesMock", () => {
|
|
8
|
+
it("should throw if mock is not valid", () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const mock = {
|
|
11
|
+
operation: {
|
|
12
|
+
id: "foo",
|
|
13
|
+
type: "query",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
const underTest = () =>
|
|
19
|
+
fetchRequestMatchesMock((mock: any), TEST_URL, null);
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
23
|
+
`"Unsupported mock operation: {\\"operation\\":{\\"id\\":\\"foo\\",\\"type\\":\\"query\\"}}"`,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should throw if input is not valid", () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const mock = "http://example.com/foo";
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const underTest = () =>
|
|
33
|
+
fetchRequestMatchesMock(
|
|
34
|
+
mock,
|
|
35
|
+
({not: "a valid request"}: any),
|
|
36
|
+
null,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
41
|
+
`"Unsupported input type"`,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe.each([TEST_URL, new URL(TEST_URL), new Request(TEST_URL)])(
|
|
46
|
+
"for valid inputs",
|
|
47
|
+
(input) => {
|
|
48
|
+
it("should return false if mock is a string and it does not match the fetched URL", () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const mock = "http://example.com/bar";
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const result = fetchRequestMatchesMock(mock, input, null);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return false if the mock is a regular expression and it doesn't match the fetched URL", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const mock = /\/bar/;
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
const result = fetchRequestMatchesMock(mock, input, null);
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return true if the mock is a string and matches the fetched URL", () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
const mock = TEST_URL;
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
const result = fetchRequestMatchesMock(mock, input, null);
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(result).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it.each([
|
|
82
|
+
/http:\/\/example.com\/foo/,
|
|
83
|
+
/queryb=elephants/,
|
|
84
|
+
/^.*#fragment$/,
|
|
85
|
+
])(
|
|
86
|
+
"should return true if the mock is a %s and matches the fetched URL",
|
|
87
|
+
(regex) => {
|
|
88
|
+
// Arrange
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
const result = fetchRequestMatchesMock(regex, input, null);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(result).toBe(true);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Request} from "node-fetch";
|
|
3
|
+
import {RespondWith} from "../../make-mock-response.js";
|
|
4
|
+
import {mockFetch} from "../mock-fetch.js";
|
|
5
|
+
|
|
6
|
+
describe("#mockFetch", () => {
|
|
7
|
+
it.each`
|
|
8
|
+
input | init
|
|
9
|
+
${"http://example.com/foo"} | ${undefined}
|
|
10
|
+
${new URL("http://example.com/foo")} | ${{method: "GET"}}
|
|
11
|
+
${new Request("http://example.com/foo")} | ${{method: "POST"}}
|
|
12
|
+
`(
|
|
13
|
+
"should reject with a useful error when there are no matching mocks for %s",
|
|
14
|
+
async ({input, init}) => {
|
|
15
|
+
// Arrange
|
|
16
|
+
const mockFn = mockFetch();
|
|
17
|
+
|
|
18
|
+
// Act
|
|
19
|
+
const result = mockFn(input, init);
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
await expect(result).rejects.toThrowErrorMatchingSnapshot();
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
describe("mockOperation", () => {
|
|
27
|
+
it("should match a similar operation", async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const mockFn = mockFetch();
|
|
30
|
+
const operation = "http://example.com/foo";
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
mockFn.mockOperation(operation, RespondWith.text("TADA!"));
|
|
34
|
+
const result = mockFn(operation, {method: "GET"});
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
await expect(result).resolves.toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should not match a different operation", async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const mockFn = mockFetch();
|
|
43
|
+
const operation = "http://example.com/foo";
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
mockFn.mockOperation(operation, RespondWith.text("TADA!"));
|
|
47
|
+
const result = mockFn("http://example.com/bar", {method: "GET"});
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
await expect(result).rejects.toThrowError();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("mockOperationOnce", () => {
|
|
55
|
+
it("should match once", async () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const mockFn = mockFetch();
|
|
58
|
+
const operation = "http://example.com/foo";
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
mockFn.mockOperationOnce(operation, RespondWith.text("TADA!"));
|
|
62
|
+
const result = mockFn(operation, {method: "GET"});
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
await expect(result).resolves.toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should only match once", async () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const mockFn = mockFetch();
|
|
71
|
+
const operation = "http://example.com/foo";
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
mockFn.mockOperationOnce(operation, RespondWith.text("TADA!"));
|
|
75
|
+
const result = Promise.all([
|
|
76
|
+
mockFn(operation, {method: "GET"}),
|
|
77
|
+
mockFn(operation, {method: "POST"}),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
await expect(result).rejects.toThrowError();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {FetchMockOperation} from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the URL from the given RequestInfo.
|
|
6
|
+
*
|
|
7
|
+
* Since we could be running in Node or in JSDOM, we don't check instance
|
|
8
|
+
* types, but just use a heuristic so that this works without knowing what
|
|
9
|
+
* was polyfilling things.
|
|
10
|
+
*/
|
|
11
|
+
const getHref = (input: RequestInfo): string => {
|
|
12
|
+
if (typeof input === "string") {
|
|
13
|
+
return input;
|
|
14
|
+
} else if (typeof input.url === "string") {
|
|
15
|
+
return input.url;
|
|
16
|
+
} else if (typeof input.href === "string") {
|
|
17
|
+
return input.href;
|
|
18
|
+
} else {
|
|
19
|
+
throw new Error(`Unsupported input type`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Determines if a given fetch invocation matches the given mock.
|
|
25
|
+
*/
|
|
26
|
+
export const fetchRequestMatchesMock = (
|
|
27
|
+
mock: FetchMockOperation,
|
|
28
|
+
input: RequestInfo,
|
|
29
|
+
init: ?RequestOptions,
|
|
30
|
+
): boolean => {
|
|
31
|
+
// Currently, we only match on the input portion.
|
|
32
|
+
// This can be a Request, a URL, or a string.
|
|
33
|
+
const href = getHref(input);
|
|
34
|
+
|
|
35
|
+
// Our mock operation is either a string for an exact match, or a regex.
|
|
36
|
+
if (typeof mock === "string") {
|
|
37
|
+
return href === mock;
|
|
38
|
+
} else if (mock instanceof RegExp) {
|
|
39
|
+
return mock.test(href);
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(`Unsupported mock operation: ${JSON.stringify(mock)}`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {fetchRequestMatchesMock} from "./fetch-request-matches-mock.js";
|
|
3
|
+
import {mockRequester} from "../mock-requester.js";
|
|
4
|
+
import type {FetchMockFn, FetchMockOperation} from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A mock for the fetch function passed to GqlRouter.
|
|
8
|
+
*/
|
|
9
|
+
export const mockFetch = (): FetchMockFn =>
|
|
10
|
+
mockRequester<FetchMockOperation, _>(
|
|
11
|
+
fetchRequestMatchesMock,
|
|
12
|
+
(input, init) =>
|
|
13
|
+
`Input: ${
|
|
14
|
+
typeof input === "string"
|
|
15
|
+
? input
|
|
16
|
+
: JSON.stringify(input, null, 2)
|
|
17
|
+
}
|
|
18
|
+
Options: ${init == null ? "None" : JSON.stringify(init, null, 2)}`,
|
|
19
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//@flow
|
|
2
|
+
import type {OperationMock} from "../types.js";
|
|
3
|
+
import type {MockResponse} from "../make-mock-response.js";
|
|
4
|
+
|
|
5
|
+
export type FetchMockOperation = RegExp | string;
|
|
6
|
+
|
|
7
|
+
type FetchMockOperationFn = (
|
|
8
|
+
operation: FetchMockOperation,
|
|
9
|
+
response: MockResponse<any>,
|
|
10
|
+
) => FetchMockFn;
|
|
11
|
+
|
|
12
|
+
export type FetchMockFn = {|
|
|
13
|
+
(input: RequestInfo, init?: RequestOptions): Promise<Response>,
|
|
14
|
+
mockOperation: FetchMockOperationFn,
|
|
15
|
+
mockOperationOnce: FetchMockOperationFn,
|
|
16
|
+
|};
|
|
17
|
+
|
|
18
|
+
export type FetchMock = OperationMock<FetchMockOperation>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import * as SetupModule from "../setup.js";
|
|
3
4
|
import * as CombineOptionsModule from "../combine-options.js";
|
|
4
5
|
import {fixtures} from "../fixtures.js";
|
|
@@ -11,6 +12,28 @@ describe("#fixtures", () => {
|
|
|
11
12
|
jest.clearAllMocks();
|
|
12
13
|
});
|
|
13
14
|
|
|
15
|
+
it("should declare a group on the configured adapter based off the given component", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const fakeGroup = {
|
|
18
|
+
closeGroup: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
const adapter = {
|
|
21
|
+
declareGroup: jest.fn().mockReturnValue(fakeGroup),
|
|
22
|
+
name: "testadapter",
|
|
23
|
+
};
|
|
24
|
+
jest.spyOn(SetupModule, "getConfiguration").mockReturnValue({
|
|
25
|
+
adapter,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
fixtures(() => "COMPONENT", jest.fn());
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(adapter.declareGroup).toHaveBeenCalledWith({
|
|
33
|
+
getDefaultTitle: expect.any(Function),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
14
37
|
it("should declare a group on the configured adapter with the given title and description", () => {
|
|
15
38
|
// Arrange
|
|
16
39
|
const fakeGroup = {
|
|
@@ -38,6 +61,7 @@ describe("#fixtures", () => {
|
|
|
38
61
|
expect(adapter.declareGroup).toHaveBeenCalledWith({
|
|
39
62
|
title: "TITLE",
|
|
40
63
|
description: "DESCRIPTION",
|
|
64
|
+
getDefaultTitle: expect.any(Function),
|
|
41
65
|
});
|
|
42
66
|
});
|
|
43
67
|
|
|
@@ -63,11 +87,11 @@ describe("#fixtures", () => {
|
|
|
63
87
|
},
|
|
64
88
|
jest.fn(),
|
|
65
89
|
);
|
|
90
|
+
const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
|
|
91
|
+
const result = getDefaultTitle();
|
|
66
92
|
|
|
67
93
|
// Assert
|
|
68
|
-
expect(
|
|
69
|
-
title: "DISPLAYNAME",
|
|
70
|
-
});
|
|
94
|
+
expect(result).toBe("DISPLAYNAME");
|
|
71
95
|
});
|
|
72
96
|
|
|
73
97
|
it("should default the title to the component.name in the absence of component.displayName", () => {
|
|
@@ -87,17 +111,12 @@ describe("#fixtures", () => {
|
|
|
87
111
|
};
|
|
88
112
|
|
|
89
113
|
// Act
|
|
90
|
-
fixtures(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
94
|
-
jest.fn(),
|
|
95
|
-
);
|
|
114
|
+
fixtures(component, jest.fn());
|
|
115
|
+
const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
|
|
116
|
+
const result = getDefaultTitle();
|
|
96
117
|
|
|
97
118
|
// Assert
|
|
98
|
-
expect(
|
|
99
|
-
title: "FUNCTIONNAME",
|
|
100
|
-
});
|
|
119
|
+
expect(result).toBe("FUNCTIONNAME");
|
|
101
120
|
});
|
|
102
121
|
|
|
103
122
|
it("should default the title to 'Component' in the absence of component.name", () => {
|
|
@@ -114,17 +133,12 @@ describe("#fixtures", () => {
|
|
|
114
133
|
});
|
|
115
134
|
|
|
116
135
|
// Act
|
|
117
|
-
fixtures(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
},
|
|
121
|
-
jest.fn(),
|
|
122
|
-
);
|
|
136
|
+
fixtures(() => "test", jest.fn());
|
|
137
|
+
const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
|
|
138
|
+
const result = getDefaultTitle();
|
|
123
139
|
|
|
124
140
|
// Assert
|
|
125
|
-
expect(
|
|
126
|
-
title: "Component",
|
|
127
|
-
});
|
|
141
|
+
expect(result).toBe("Component");
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
it("should invoke the passed fn with function argument", () => {
|
|
@@ -271,7 +285,7 @@ describe("#fixtures", () => {
|
|
|
271
285
|
});
|
|
272
286
|
|
|
273
287
|
describe("injected fixture fn", () => {
|
|
274
|
-
it("should call group.declareFixture with description
|
|
288
|
+
it("should call group.declareFixture with description, props getter, and component", () => {
|
|
275
289
|
// Arrange
|
|
276
290
|
const fakeGroup = {
|
|
277
291
|
declareFixture: jest.fn(),
|
|
@@ -306,6 +320,36 @@ describe("#fixtures", () => {
|
|
|
306
320
|
});
|
|
307
321
|
});
|
|
308
322
|
|
|
323
|
+
it("should call group.declareFixture with component if component is forward ref", () => {
|
|
324
|
+
// Arrange
|
|
325
|
+
const fakeGroup = {
|
|
326
|
+
declareFixture: jest.fn(),
|
|
327
|
+
closeGroup: jest.fn(),
|
|
328
|
+
};
|
|
329
|
+
const adapter = {
|
|
330
|
+
declareGroup: jest.fn().mockReturnValue(fakeGroup),
|
|
331
|
+
name: "testadapter",
|
|
332
|
+
};
|
|
333
|
+
jest.spyOn(SetupModule, "getConfiguration").mockReturnValue({
|
|
334
|
+
adapter,
|
|
335
|
+
});
|
|
336
|
+
const component = React.forwardRef((props, ref) => (
|
|
337
|
+
<div {...props} ref={ref} />
|
|
338
|
+
));
|
|
339
|
+
|
|
340
|
+
// Act
|
|
341
|
+
fixtures(component, (fixture) => {
|
|
342
|
+
fixture("FIXTURE_DESCRIPTION", {these: "areProps"});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Assert
|
|
346
|
+
expect(fakeGroup.declareFixture).toHaveBeenCalledWith({
|
|
347
|
+
description: "FIXTURE_DESCRIPTION",
|
|
348
|
+
getProps: expect.any(Function),
|
|
349
|
+
component,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
309
353
|
it("should pass wrapper component to group.declareFixture", () => {
|
|
310
354
|
// Arrange
|
|
311
355
|
const fakeGroup = {
|
|
@@ -12,6 +12,9 @@ describe("AdapterGroup", () => {
|
|
|
12
12
|
new AdapterGroup(badCloseGroupFn, {
|
|
13
13
|
title: "TITLE",
|
|
14
14
|
description: null,
|
|
15
|
+
getDefaultTitle: () => {
|
|
16
|
+
throw new Error("NOT IMPLEMENTED");
|
|
17
|
+
},
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
expect(act).toThrowErrorMatchingInlineSnapshot(
|
|
@@ -40,6 +43,9 @@ describe("AdapterGroup", () => {
|
|
|
40
43
|
const groupOptions = {
|
|
41
44
|
title: "TITLE",
|
|
42
45
|
description: null,
|
|
46
|
+
getDefaultTitle: () => {
|
|
47
|
+
throw new Error("NOT IMPLEMENTED");
|
|
48
|
+
},
|
|
43
49
|
};
|
|
44
50
|
const adapterGroup = new AdapterGroup(closeGroupFn, groupOptions);
|
|
45
51
|
|
|
@@ -56,6 +62,9 @@ describe("AdapterGroup", () => {
|
|
|
56
62
|
const groupOptions = {
|
|
57
63
|
title: "TITLE",
|
|
58
64
|
description: null,
|
|
65
|
+
getDefaultTitle: () => {
|
|
66
|
+
throw new Error("NOT IMPLEMENTED");
|
|
67
|
+
},
|
|
59
68
|
};
|
|
60
69
|
const adapterSpecificOptions = {
|
|
61
70
|
adapterSpecificOption: "adapterSpecificOption",
|
|
@@ -79,6 +88,9 @@ describe("AdapterGroup", () => {
|
|
|
79
88
|
const groupOptions = {
|
|
80
89
|
title: "TITLE",
|
|
81
90
|
description: "DESCRIPTION",
|
|
91
|
+
getDefaultTitle: () => {
|
|
92
|
+
throw new Error("NOT IMPLEMENTED");
|
|
93
|
+
},
|
|
82
94
|
};
|
|
83
95
|
const adapterGroup = new AdapterGroup(closeGroupFn, groupOptions);
|
|
84
96
|
const fixture = {
|
|
@@ -103,6 +115,9 @@ describe("AdapterGroup", () => {
|
|
|
103
115
|
const groupOptions = {
|
|
104
116
|
title: "TITLE",
|
|
105
117
|
description: null,
|
|
118
|
+
getDefaultTitle: () => {
|
|
119
|
+
throw new Error("NOT IMPLEMENTED");
|
|
120
|
+
},
|
|
106
121
|
};
|
|
107
122
|
const adapterGroup = new AdapterGroup(closeGroupFn, groupOptions);
|
|
108
123
|
adapterGroup.closeGroup();
|
|
@@ -126,6 +141,9 @@ describe("AdapterGroup", () => {
|
|
|
126
141
|
const groupOptions = {
|
|
127
142
|
title: "TITLE",
|
|
128
143
|
description: null,
|
|
144
|
+
getDefaultTitle: () => {
|
|
145
|
+
throw new Error("NOT IMPLEMENTED");
|
|
146
|
+
},
|
|
129
147
|
};
|
|
130
148
|
const adapterGroup = new AdapterGroup(
|
|
131
149
|
closeGroupFn,
|
|
@@ -147,6 +165,9 @@ describe("AdapterGroup", () => {
|
|
|
147
165
|
const groupOptions = {
|
|
148
166
|
title: "TITLE",
|
|
149
167
|
description: "DESCRIPTION",
|
|
168
|
+
getDefaultTitle: () => {
|
|
169
|
+
throw new Error("NOT IMPLEMENTED");
|
|
170
|
+
},
|
|
150
171
|
};
|
|
151
172
|
const adapterGroup = new AdapterGroup(closeGroupFn, groupOptions);
|
|
152
173
|
const fixture1 = {
|
|
@@ -178,6 +199,9 @@ describe("AdapterGroup", () => {
|
|
|
178
199
|
const groupOptions = {
|
|
179
200
|
title: "TITLE",
|
|
180
201
|
description: null,
|
|
202
|
+
getDefaultTitle: () => {
|
|
203
|
+
throw new Error("NOT IMPLEMENTED");
|
|
204
|
+
},
|
|
181
205
|
};
|
|
182
206
|
const adapterGroup = new AdapterGroup(closeGroupFn, groupOptions);
|
|
183
207
|
adapterGroup.closeGroup();
|
|
@@ -56,6 +56,9 @@ describe("Adapter", () => {
|
|
|
56
56
|
const options = {
|
|
57
57
|
title: "group_title",
|
|
58
58
|
description: "group_description",
|
|
59
|
+
getDefaultTitle: () => {
|
|
60
|
+
throw new Error("NOT IMPLEMENTED");
|
|
61
|
+
},
|
|
59
62
|
};
|
|
60
63
|
const adapterGroupSpy = jest
|
|
61
64
|
.spyOn(AdapterGroupModule, "AdapterGroup")
|
|
@@ -82,6 +85,9 @@ describe("Adapter", () => {
|
|
|
82
85
|
const result = adapter.declareGroup({
|
|
83
86
|
title: "group_title",
|
|
84
87
|
description: "group_description",
|
|
88
|
+
getDefaultTitle: () => {
|
|
89
|
+
throw new Error("NOT IMPLEMENTED");
|
|
90
|
+
},
|
|
85
91
|
});
|
|
86
92
|
|
|
87
93
|
// Assert
|
|
@@ -26,7 +26,7 @@ export type StorybookOptions = {|
|
|
|
26
26
|
|};
|
|
27
27
|
|
|
28
28
|
type DefaultExport = {|
|
|
29
|
-
title
|
|
29
|
+
title?: ?string,
|
|
30
30
|
...StorybookOptions,
|
|
31
31
|
|};
|
|
32
32
|
|
|
@@ -47,6 +47,9 @@ export const getAdapter: AdapterFactory<StorybookOptions, Exports<any>> = (
|
|
|
47
47
|
{
|
|
48
48
|
title,
|
|
49
49
|
description: groupDescription,
|
|
50
|
+
// We don't use the default title in Storybook as storybook
|
|
51
|
+
// will generate titles for us if we pass a nullish title.
|
|
52
|
+
getDefaultTitle: _,
|
|
50
53
|
}: $ReadOnly<AdapterGroupOptions>,
|
|
51
54
|
adapterOptions: ?$ReadOnly<StorybookOptions>,
|
|
52
55
|
declaredFixtures: $ReadOnlyArray<AdapterFixtureOptions<TProps>>,
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
|
|
4
4
|
import {setupFixtures, fixtures, adapters} from "../index.js";
|
|
5
5
|
|
|
6
|
-
// Normally would call setup from the storybook.
|
|
6
|
+
// Normally would call setup from the storybook.preview.js for a project.
|
|
7
7
|
setupFixtures({
|
|
8
8
|
adapter: adapters.storybook(),
|
|
9
9
|
});
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
|
|
4
4
|
import {setupFixtures, fixtures, adapters} from "../index.js";
|
|
5
5
|
|
|
6
|
-
// Normally would call setup from the storybook.
|
|
6
|
+
// Normally would call setup from the storybook.preview.js for a project.
|
|
7
7
|
setupFixtures({
|
|
8
8
|
adapter: adapters.storybook(),
|
|
9
9
|
});
|
package/src/fixtures/fixtures.js
CHANGED
|
@@ -9,6 +9,45 @@ type FixtureProps<TProps: {...}> =
|
|
|
9
9
|
| $ReadOnly<TProps>
|
|
10
10
|
| ((options: $ReadOnly<GetPropsOptions>) => $ReadOnly<TProps>);
|
|
11
11
|
|
|
12
|
+
const normalizeOptions = <TProps: {...}>(
|
|
13
|
+
componentOrOptions:
|
|
14
|
+
| React.ComponentType<TProps>
|
|
15
|
+
| $ReadOnly<FixturesOptions<TProps>>,
|
|
16
|
+
): $ReadOnly<FixturesOptions<TProps>> => {
|
|
17
|
+
// To differentiate between a React component and a FixturesOptions object,
|
|
18
|
+
// we have to do some type checking.
|
|
19
|
+
//
|
|
20
|
+
// Alternatives I considered were:
|
|
21
|
+
// - Use an additional parameter for the options and then do an arg number
|
|
22
|
+
// check, but that always makes typing a function harder and often breaks
|
|
23
|
+
// types. I didn't want that battle today.
|
|
24
|
+
// - Use a tuple when providing component and options with the first element
|
|
25
|
+
// being the component and the second being the options. However that
|
|
26
|
+
// feels like an obscure API even though it's really easy to do the
|
|
27
|
+
// typing.
|
|
28
|
+
if (
|
|
29
|
+
// Most React components, whether functional or class-based, are
|
|
30
|
+
// inherently functions in JavaScript, so a check for functions is
|
|
31
|
+
// usually sufficient.
|
|
32
|
+
typeof componentOrOptions === "function" ||
|
|
33
|
+
// However, the return of React.forwardRef is not a function,
|
|
34
|
+
// so we also have to cope with that.
|
|
35
|
+
// A forwardRef has $$typeof = Symbol(react.forward_ref) and a
|
|
36
|
+
// render function.
|
|
37
|
+
// $FlowIgnore[prop-missing]
|
|
38
|
+
typeof componentOrOptions.render === "function"
|
|
39
|
+
) {
|
|
40
|
+
return {
|
|
41
|
+
// $FlowIgnore[incompatible-return]
|
|
42
|
+
component: componentOrOptions,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// We can't test for React.ComponentType at runtime.
|
|
46
|
+
// Let's assume our simple heuristic above is sufficient.
|
|
47
|
+
// $FlowIgnore[incompatible-return]
|
|
48
|
+
return componentOrOptions;
|
|
49
|
+
};
|
|
50
|
+
|
|
12
51
|
/**
|
|
13
52
|
* Describe a group of fixtures for a given component.
|
|
14
53
|
*
|
|
@@ -28,7 +67,9 @@ type FixtureProps<TProps: {...}> =
|
|
|
28
67
|
* its interface.
|
|
29
68
|
*/
|
|
30
69
|
export const fixtures = <TProps: {...}>(
|
|
31
|
-
|
|
70
|
+
componentOrOptions:
|
|
71
|
+
| React.ComponentType<TProps>
|
|
72
|
+
| $ReadOnly<FixturesOptions<TProps>>,
|
|
32
73
|
fn: (
|
|
33
74
|
fixture: (
|
|
34
75
|
description: string,
|
|
@@ -38,18 +79,21 @@ export const fixtures = <TProps: {...}>(
|
|
|
38
79
|
) => void,
|
|
39
80
|
): ?$ReadOnly<mixed> => {
|
|
40
81
|
const {adapter, defaultAdapterOptions} = getConfiguration();
|
|
82
|
+
|
|
41
83
|
const {
|
|
42
84
|
title,
|
|
43
85
|
component,
|
|
44
86
|
description: groupDescription,
|
|
45
87
|
defaultWrapper,
|
|
46
88
|
additionalAdapterOptions,
|
|
47
|
-
} =
|
|
89
|
+
} = normalizeOptions(componentOrOptions);
|
|
48
90
|
|
|
49
91
|
// 1. Create a new adapter group.
|
|
50
92
|
const group = adapter.declareGroup<TProps>({
|
|
51
|
-
title
|
|
93
|
+
title,
|
|
52
94
|
description: groupDescription,
|
|
95
|
+
getDefaultTitle: () =>
|
|
96
|
+
component.displayName || component.name || "Component",
|
|
53
97
|
});
|
|
54
98
|
|
|
55
99
|
// 2. Invoke fn with a function that can add a new fixture.
|
package/src/fixtures/types.js
CHANGED
|
@@ -81,13 +81,22 @@ export type AdapterFixtureOptions<TProps: {...}> = {|
|
|
|
81
81
|
export type AdapterGroupOptions = {|
|
|
82
82
|
/**
|
|
83
83
|
* The title of the group.
|
|
84
|
+
*
|
|
85
|
+
* If omitted, the adapter is free to generate a default or ask for one
|
|
86
|
+
* using the passed getDefaultTitle() function.
|
|
84
87
|
*/
|
|
85
|
-
+title: string,
|
|
88
|
+
+title: ?string,
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
91
|
* Description of the group.
|
|
89
92
|
*/
|
|
90
93
|
+description: ?string,
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Function that will generate a default title if an adapter cannot
|
|
97
|
+
* generate its own.
|
|
98
|
+
*/
|
|
99
|
+
+getDefaultTitle: () => string,
|
|
91
100
|
|};
|
|
92
101
|
|
|
93
102
|
/**
|