@khanacademy/wonder-blocks-testing 2.0.8 → 3.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @khanacademy/wonder-blocks-testing
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 9a43cc06: Allow for autogenerating titles in Storybook
8
+
9
+ ### Patch Changes
10
+
11
+ - 222cb8db: Add simplified signature for common usage of `fixtures` function
12
+
3
13
  ## 2.0.8
4
14
 
5
15
  ### 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,31 @@ 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. Since all React components, whether
271
+ // functional or class-based, are inherently functions in JavaScript
272
+ // this should do the trick without relying on internal React details like
273
+ // protoype.isReactComponent. This should be sufficient for our purposes.
274
+ // Alternatives I considered were:
275
+ // - Use an additional parameter for the options and then do an arg number
276
+ // check, but that always makes typing a function harder and often breaks
277
+ // types. I didn't want that battle today.
278
+ // - Use a tuple when providing component and options with the first element
279
+ // being the component and the second being the options. However that
280
+ // feels like an obscure API even though it's really easy to do the
281
+ // typing.
282
+ if (typeof componentOrOptions === "function") {
283
+ return {
284
+ component: componentOrOptions
285
+ };
286
+ } // We can't test for React.ComponentType at runtime.
287
+ // Let's assume our simple heuristic above is sufficient.
288
+ // $FlowIgnore[incompatible-return]
289
+
290
+
291
+ return componentOrOptions;
292
+ };
265
293
  /**
266
294
  * Describe a group of fixtures for a given component.
267
295
  *
@@ -280,7 +308,9 @@ const combineOptions = (...toBeCombined) => {
280
308
  * storybook, the popular framework, uses both default and named exports for
281
309
  * its interface.
282
310
  */
283
- const fixtures = (options, fn) => {
311
+
312
+
313
+ const fixtures = (componentOrOptions, fn) => {
284
314
  var _additionalAdapterOpt;
285
315
 
286
316
  const {
@@ -293,11 +323,12 @@ const fixtures = (options, fn) => {
293
323
  description: groupDescription,
294
324
  defaultWrapper,
295
325
  additionalAdapterOptions
296
- } = options; // 1. Create a new adapter group.
326
+ } = normalizeOptions(componentOrOptions); // 1. Create a new adapter group.
297
327
 
298
328
  const group = adapter.declareGroup({
299
- title: title || component.displayName || component.name || "Component",
300
- description: groupDescription
329
+ title,
330
+ description: groupDescription,
331
+ getDefaultTitle: () => component.displayName || component.name || "Component"
301
332
  }); // 2. Invoke fn with a function that can add a new fixture.
302
333
 
303
334
  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,31 @@ 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. Since all React components, whether
332
+ // functional or class-based, are inherently functions in JavaScript
333
+ // this should do the trick without relying on internal React details like
334
+ // protoype.isReactComponent. This should be sufficient for our purposes.
335
+ // Alternatives I considered were:
336
+ // - Use an additional parameter for the options and then do an arg number
337
+ // check, but that always makes typing a function harder and often breaks
338
+ // types. I didn't want that battle today.
339
+ // - Use a tuple when providing component and options with the first element
340
+ // being the component and the second being the options. However that
341
+ // feels like an obscure API even though it's really easy to do the
342
+ // typing.
343
+ if (typeof componentOrOptions === "function") {
344
+ return {
345
+ component: componentOrOptions
346
+ };
347
+ } // We can't test for React.ComponentType at runtime.
348
+ // Let's assume our simple heuristic above is sufficient.
349
+ // $FlowIgnore[incompatible-return]
350
+
351
+
352
+ return componentOrOptions;
353
+ };
326
354
  /**
327
355
  * Describe a group of fixtures for a given component.
328
356
  *
@@ -341,7 +369,9 @@ const getAdapter = (MountingComponent = null) => new _adapter_js__WEBPACK_IMPORT
341
369
  * storybook, the popular framework, uses both default and named exports for
342
370
  * its interface.
343
371
  */
344
- const fixtures = (options, fn) => {
372
+
373
+
374
+ const fixtures = (componentOrOptions, fn) => {
345
375
  var _additionalAdapterOpt;
346
376
 
347
377
  const {
@@ -354,11 +384,12 @@ const fixtures = (options, fn) => {
354
384
  description: groupDescription,
355
385
  defaultWrapper,
356
386
  additionalAdapterOptions
357
- } = options; // 1. Create a new adapter group.
387
+ } = normalizeOptions(componentOrOptions); // 1. Create a new adapter group.
358
388
 
359
389
  const group = adapter.declareGroup({
360
- title: title || component.displayName || component.name || "Component",
361
- description: groupDescription
390
+ title,
391
+ description: groupDescription,
392
+ getDefaultTitle: () => component.displayName || component.name || "Component"
362
393
  }); // 2. Invoke fn with a function that can add a new fixture.
363
394
 
364
395
  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.8",
3
+ "version": "3.0.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -11,6 +11,28 @@ describe("#fixtures", () => {
11
11
  jest.clearAllMocks();
12
12
  });
13
13
 
14
+ it("should declare a group on the configured adapter based off the given component", () => {
15
+ // Arrange
16
+ const fakeGroup = {
17
+ closeGroup: jest.fn(),
18
+ };
19
+ const adapter = {
20
+ declareGroup: jest.fn().mockReturnValue(fakeGroup),
21
+ name: "testadapter",
22
+ };
23
+ jest.spyOn(SetupModule, "getConfiguration").mockReturnValue({
24
+ adapter,
25
+ });
26
+
27
+ // Act
28
+ fixtures(() => "COMPONENT", jest.fn());
29
+
30
+ // Assert
31
+ expect(adapter.declareGroup).toHaveBeenCalledWith({
32
+ getDefaultTitle: expect.any(Function),
33
+ });
34
+ });
35
+
14
36
  it("should declare a group on the configured adapter with the given title and description", () => {
15
37
  // Arrange
16
38
  const fakeGroup = {
@@ -38,6 +60,7 @@ describe("#fixtures", () => {
38
60
  expect(adapter.declareGroup).toHaveBeenCalledWith({
39
61
  title: "TITLE",
40
62
  description: "DESCRIPTION",
63
+ getDefaultTitle: expect.any(Function),
41
64
  });
42
65
  });
43
66
 
@@ -63,11 +86,11 @@ describe("#fixtures", () => {
63
86
  },
64
87
  jest.fn(),
65
88
  );
89
+ const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
90
+ const result = getDefaultTitle();
66
91
 
67
92
  // Assert
68
- expect(adapter.declareGroup).toHaveBeenCalledWith({
69
- title: "DISPLAYNAME",
70
- });
93
+ expect(result).toBe("DISPLAYNAME");
71
94
  });
72
95
 
73
96
  it("should default the title to the component.name in the absence of component.displayName", () => {
@@ -87,17 +110,12 @@ describe("#fixtures", () => {
87
110
  };
88
111
 
89
112
  // Act
90
- fixtures(
91
- {
92
- component,
93
- },
94
- jest.fn(),
95
- );
113
+ fixtures(component, jest.fn());
114
+ const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
115
+ const result = getDefaultTitle();
96
116
 
97
117
  // Assert
98
- expect(adapter.declareGroup).toHaveBeenCalledWith({
99
- title: "FUNCTIONNAME",
100
- });
118
+ expect(result).toBe("FUNCTIONNAME");
101
119
  });
102
120
 
103
121
  it("should default the title to 'Component' in the absence of component.name", () => {
@@ -114,17 +132,12 @@ describe("#fixtures", () => {
114
132
  });
115
133
 
116
134
  // Act
117
- fixtures(
118
- {
119
- component: ({}: any),
120
- },
121
- jest.fn(),
122
- );
135
+ fixtures(() => "test", jest.fn());
136
+ const {getDefaultTitle} = adapter.declareGroup.mock.calls[0][0];
137
+ const result = getDefaultTitle();
123
138
 
124
139
  // Assert
125
- expect(adapter.declareGroup).toHaveBeenCalledWith({
126
- title: "Component",
127
- });
140
+ expect(result).toBe("Component");
128
141
  });
129
142
 
130
143
  it("should invoke the passed fn with function argument", () => {
@@ -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,35 @@ 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. Since all React components, whether
19
+ // functional or class-based, are inherently functions in JavaScript
20
+ // this should do the trick without relying on internal React details like
21
+ // protoype.isReactComponent. This should be sufficient for our purposes.
22
+ // Alternatives I considered were:
23
+ // - Use an additional parameter for the options and then do an arg number
24
+ // check, but that always makes typing a function harder and often breaks
25
+ // types. I didn't want that battle today.
26
+ // - Use a tuple when providing component and options with the first element
27
+ // being the component and the second being the options. However that
28
+ // feels like an obscure API even though it's really easy to do the
29
+ // typing.
30
+ if (typeof componentOrOptions === "function") {
31
+ return {
32
+ component: componentOrOptions,
33
+ };
34
+ }
35
+ // We can't test for React.ComponentType at runtime.
36
+ // Let's assume our simple heuristic above is sufficient.
37
+ // $FlowIgnore[incompatible-return]
38
+ return componentOrOptions;
39
+ };
40
+
12
41
  /**
13
42
  * Describe a group of fixtures for a given component.
14
43
  *
@@ -28,7 +57,9 @@ type FixtureProps<TProps: {...}> =
28
57
  * its interface.
29
58
  */
30
59
  export const fixtures = <TProps: {...}>(
31
- options: $ReadOnly<FixturesOptions<TProps>>,
60
+ componentOrOptions:
61
+ | React.ComponentType<TProps>
62
+ | $ReadOnly<FixturesOptions<TProps>>,
32
63
  fn: (
33
64
  fixture: (
34
65
  description: string,
@@ -38,18 +69,21 @@ export const fixtures = <TProps: {...}>(
38
69
  ) => void,
39
70
  ): ?$ReadOnly<mixed> => {
40
71
  const {adapter, defaultAdapterOptions} = getConfiguration();
72
+
41
73
  const {
42
74
  title,
43
75
  component,
44
76
  description: groupDescription,
45
77
  defaultWrapper,
46
78
  additionalAdapterOptions,
47
- } = options;
79
+ } = normalizeOptions(componentOrOptions);
48
80
 
49
81
  // 1. Create a new adapter group.
50
82
  const group = adapter.declareGroup<TProps>({
51
- title: title || component.displayName || component.name || "Component",
83
+ title,
52
84
  description: groupDescription,
85
+ getDefaultTitle: () =>
86
+ component.displayName || component.name || "Component",
53
87
  });
54
88
 
55
89
  // 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
  /**