@khanacademy/wonder-blocks-testing 2.0.7 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @khanacademy/wonder-blocks-testing
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6e4fbeed: Make sure simplified fixtures call copes with return values from React.forwardRef
8
+
9
+ ## 3.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - 9a43cc06: Allow for autogenerating titles in Storybook
14
+
15
+ ### Patch Changes
16
+
17
+ - 222cb8db: Add simplified signature for common usage of `fixtures` function
18
+
19
+ ## 2.0.8
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [34407c4a]
24
+ - @khanacademy/wonder-blocks-data@7.0.0
25
+
3
26
  ## 2.0.7
4
27
 
5
28
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -118,7 +118,10 @@ class Adapter {
118
118
  */
119
119
  const getAdapter = (MountingComponent = null) => new Adapter("storybook", ({
120
120
  title,
121
- description: groupDescription
121
+ description: groupDescription,
122
+ // We don't use the default title in Storybook as storybook
123
+ // will generate titles for us if we pass a nullish title.
124
+ getDefaultTitle: _
122
125
  }, adapterOptions, declaredFixtures) => {
123
126
  const templateMap = new WeakMap();
124
127
 
@@ -262,6 +265,38 @@ const combineOptions = (...toBeCombined) => {
262
265
  return combined;
263
266
  };
264
267
 
268
+ const normalizeOptions = componentOrOptions => {
269
+ // To differentiate between a React component and a FixturesOptions object,
270
+ // we have to do some type checking.
271
+ //
272
+ // Alternatives I considered were:
273
+ // - Use an additional parameter for the options and then do an arg number
274
+ // check, but that always makes typing a function harder and often breaks
275
+ // types. I didn't want that battle today.
276
+ // - Use a tuple when providing component and options with the first element
277
+ // being the component and the second being the options. However that
278
+ // feels like an obscure API even though it's really easy to do the
279
+ // typing.
280
+ if ( // Most React components, whether functional or class-based, are
281
+ // inherently functions in JavaScript, so a check for functions is
282
+ // usually sufficient.
283
+ typeof componentOrOptions === "function" || // However, the return of React.forwardRef is not a function,
284
+ // so we also have to cope with that.
285
+ // A forwardRef has $$typeof = Symbol(react.forward_ref) and a
286
+ // render function.
287
+ // $FlowIgnore[prop-missing]
288
+ typeof componentOrOptions.render === "function") {
289
+ return {
290
+ // $FlowIgnore[incompatible-return]
291
+ component: componentOrOptions
292
+ };
293
+ } // We can't test for React.ComponentType at runtime.
294
+ // Let's assume our simple heuristic above is sufficient.
295
+ // $FlowIgnore[incompatible-return]
296
+
297
+
298
+ return componentOrOptions;
299
+ };
265
300
  /**
266
301
  * Describe a group of fixtures for a given component.
267
302
  *
@@ -280,7 +315,9 @@ const combineOptions = (...toBeCombined) => {
280
315
  * storybook, the popular framework, uses both default and named exports for
281
316
  * its interface.
282
317
  */
283
- const fixtures = (options, fn) => {
318
+
319
+
320
+ const fixtures = (componentOrOptions, fn) => {
284
321
  var _additionalAdapterOpt;
285
322
 
286
323
  const {
@@ -293,11 +330,12 @@ const fixtures = (options, fn) => {
293
330
  description: groupDescription,
294
331
  defaultWrapper,
295
332
  additionalAdapterOptions
296
- } = options; // 1. Create a new adapter group.
333
+ } = normalizeOptions(componentOrOptions); // 1. Create a new adapter group.
297
334
 
298
335
  const group = adapter.declareGroup({
299
- title: title || component.displayName || component.name || "Component",
300
- description: groupDescription
336
+ title,
337
+ description: groupDescription,
338
+ getDefaultTitle: () => component.displayName || component.name || "Component"
301
339
  }); // 2. Invoke fn with a function that can add a new fixture.
302
340
 
303
341
  const addFixture = (description, props, wrapper = null) => {
package/dist/index.js CHANGED
@@ -247,7 +247,10 @@ __webpack_require__.r(__webpack_exports__);
247
247
  */
248
248
  const getAdapter = (MountingComponent = null) => new _adapter_js__WEBPACK_IMPORTED_MODULE_2__[/* Adapter */ "a"]("storybook", ({
249
249
  title,
250
- description: groupDescription
250
+ description: groupDescription,
251
+ // We don't use the default title in Storybook as storybook
252
+ // will generate titles for us if we pass a nullish title.
253
+ getDefaultTitle: _
251
254
  }, adapterOptions, declaredFixtures) => {
252
255
  const templateMap = new WeakMap();
253
256
 
@@ -323,6 +326,38 @@ const getAdapter = (MountingComponent = null) => new _adapter_js__WEBPACK_IMPORT
323
326
 
324
327
 
325
328
 
329
+ const normalizeOptions = componentOrOptions => {
330
+ // To differentiate between a React component and a FixturesOptions object,
331
+ // we have to do some type checking.
332
+ //
333
+ // Alternatives I considered were:
334
+ // - Use an additional parameter for the options and then do an arg number
335
+ // check, but that always makes typing a function harder and often breaks
336
+ // types. I didn't want that battle today.
337
+ // - Use a tuple when providing component and options with the first element
338
+ // being the component and the second being the options. However that
339
+ // feels like an obscure API even though it's really easy to do the
340
+ // typing.
341
+ if ( // Most React components, whether functional or class-based, are
342
+ // inherently functions in JavaScript, so a check for functions is
343
+ // usually sufficient.
344
+ typeof componentOrOptions === "function" || // However, the return of React.forwardRef is not a function,
345
+ // so we also have to cope with that.
346
+ // A forwardRef has $$typeof = Symbol(react.forward_ref) and a
347
+ // render function.
348
+ // $FlowIgnore[prop-missing]
349
+ typeof componentOrOptions.render === "function") {
350
+ return {
351
+ // $FlowIgnore[incompatible-return]
352
+ component: componentOrOptions
353
+ };
354
+ } // We can't test for React.ComponentType at runtime.
355
+ // Let's assume our simple heuristic above is sufficient.
356
+ // $FlowIgnore[incompatible-return]
357
+
358
+
359
+ return componentOrOptions;
360
+ };
326
361
  /**
327
362
  * Describe a group of fixtures for a given component.
328
363
  *
@@ -341,7 +376,9 @@ const getAdapter = (MountingComponent = null) => new _adapter_js__WEBPACK_IMPORT
341
376
  * storybook, the popular framework, uses both default and named exports for
342
377
  * its interface.
343
378
  */
344
- const fixtures = (options, fn) => {
379
+
380
+
381
+ const fixtures = (componentOrOptions, fn) => {
345
382
  var _additionalAdapterOpt;
346
383
 
347
384
  const {
@@ -354,11 +391,12 @@ const fixtures = (options, fn) => {
354
391
  description: groupDescription,
355
392
  defaultWrapper,
356
393
  additionalAdapterOptions
357
- } = options; // 1. Create a new adapter group.
394
+ } = normalizeOptions(componentOrOptions); // 1. Create a new adapter group.
358
395
 
359
396
  const group = adapter.declareGroup({
360
- title: title || component.displayName || component.name || "Component",
361
- description: groupDescription
397
+ title,
398
+ description: groupDescription,
399
+ getDefaultTitle: () => component.displayName || component.name || "Component"
362
400
  }); // 2. Invoke fn with a function that can add a new fixture.
363
401
 
364
402
  const addFixture = (description, props, wrapper = null) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-testing",
3
- "version": "2.0.7",
3
+ "version": "3.0.1",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@babel/runtime": "^7.16.3",
17
- "@khanacademy/wonder-blocks-data": "^6.0.1"
17
+ "@khanacademy/wonder-blocks-data": "^7.0.0"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@khanacademy/wonder-stuff-core": "^0.1.2",
@@ -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(adapter.declareGroup).toHaveBeenCalledWith({
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
- component,
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(adapter.declareGroup).toHaveBeenCalledWith({
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
- component: ({}: any),
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(adapter.declareGroup).toHaveBeenCalledWith({
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 and props getter", () => {
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: string,
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.main.js for a project.
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.main.js for a project.
6
+ // Normally would call setup from the storybook.preview.js for a project.
7
7
  setupFixtures({
8
8
  adapter: adapters.storybook(),
9
9
  });
@@ -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
- options: $ReadOnly<FixturesOptions<TProps>>,
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
- } = options;
89
+ } = normalizeOptions(componentOrOptions);
48
90
 
49
91
  // 1. Create a new adapter group.
50
92
  const group = adapter.declareGroup<TProps>({
51
- title: title || component.displayName || component.name || "Component",
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.
@@ -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
  /**