@khanacademy/wonder-blocks-testing 4.0.0 → 4.0.3

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,33 @@
1
1
  # @khanacademy/wonder-blocks-testing
2
2
 
3
+ ## 4.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [e5fa4d9e]
8
+ - @khanacademy/wonder-blocks-data@8.0.1
9
+
10
+ ## 4.0.2
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [1385f468]
15
+ - Updated dependencies [0720470e]
16
+ - Updated dependencies [0720470e]
17
+ - Updated dependencies [cf9ed87f]
18
+ - Updated dependencies [b882b082]
19
+ - Updated dependencies [0720470e]
20
+ - Updated dependencies [75c10036]
21
+ - Updated dependencies [a85f2f3a]
22
+ - Updated dependencies [0720470e]
23
+ - @khanacademy/wonder-blocks-data@8.0.0
24
+
25
+ ## 4.0.1
26
+
27
+ ### Patch Changes
28
+
29
+ - @khanacademy/wonder-blocks-data@7.0.1
30
+
3
31
  ## 4.0.0
4
32
 
5
33
  ### Major Changes
package/dist/es/index.js CHANGED
@@ -3,17 +3,7 @@ import * as React from 'react';
3
3
  import { action } from '@storybook/addon-actions';
4
4
  import { clone } from '@khanacademy/wonder-stuff-core';
5
5
 
6
- /**
7
- * Simple adapter group implementation.
8
- */
9
6
  class AdapterGroup {
10
- /**
11
- * Create an adapter group.
12
- *
13
- * @param {CloseGroupFn<TProps, Options, Exports>} closeGroupFn A function
14
- * to invoke when the group is closed.
15
- * @param {AdapterGroupOptions} options The options for the group.
16
- */
17
7
  constructor(closeGroupFn, _options) {
18
8
  this.closeGroup = (adapterOptions = null) => {
19
9
  if (this._closeGroupFn == null) {
@@ -51,29 +41,10 @@ class AdapterGroup {
51
41
  this._options = _options;
52
42
  this._fixtures = [];
53
43
  }
54
- /**
55
- * Close the group.
56
- *
57
- * This declares that no more fixtures are to be added to the group,
58
- * and will call the parent adapter with the declared fixtures so that they
59
- * can be adapted for the target fixture framework, such as Storybook.
60
- */
61
-
62
44
 
63
45
  }
64
46
 
65
- /**
66
- * Class for implementing a custom adapter.
67
- */
68
47
  class Adapter {
69
- /**
70
- * @param {string} name The name of the adapter.
71
- * @param {CloseGroupFn<any, Options, Exports>} closeGroupFn The function
72
- * an adapter group should call when the group is closed. This is invoked
73
- * by an adapter group when it is closed. This function is where an
74
- * adapter implements the logic to generate the actual fixtures for the
75
- * adapter's target framework.
76
- */
77
48
  constructor(name, closeGroupFn) {
78
49
  if (typeof name !== "string") {
79
50
  throw new TypeError("name must be a string");
@@ -90,22 +61,10 @@ class Adapter {
90
61
  this._name = name;
91
62
  this._closeGroupFn = closeGroupFn;
92
63
  }
93
- /**
94
- * The name of the adapter.
95
- */
96
-
97
64
 
98
65
  get name() {
99
66
  return this._name;
100
67
  }
101
- /**
102
- * Declare a new fixture group.
103
- *
104
- * @param {AdapterGroupOptions} options The options describing the fixture
105
- * group.
106
- * @returns {AdapterGroupInterface} The new fixture group.
107
- */
108
-
109
68
 
110
69
  declareGroup(options) {
111
70
  return new AdapterGroup(this._closeGroupFn, options);
@@ -113,14 +72,9 @@ class Adapter {
113
72
 
114
73
  }
115
74
 
116
- /**
117
- * Get a fixture framework adapter for Storybook support.
118
- */
119
75
  const getAdapter = (MountingComponent = null) => new Adapter("storybook", ({
120
76
  title,
121
77
  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
78
  getDefaultTitle: _
125
79
  }, adapterOptions, declaredFixtures) => {
126
80
  const templateMap = new WeakMap();
@@ -133,43 +87,22 @@ const getAdapter = (MountingComponent = null) => new Adapter("storybook", ({
133
87
  component: Component
134
88
  }, i) => {
135
89
  const storyName = `${i + 1} ${description}`;
136
- const exportName = storyName // Make word boundaries start with an upper case letter.
137
- .replace(/\b\w/g, c => c.toUpperCase()) // Remove all non-alphanumeric characters.
138
- .replace(/[^\w]+/g, "") // Remove all underscores.
139
- .replace(/[_]+/g, ""); // We create a “template” of how args map to rendering
140
- // for each type of component as the component here could
141
- // be the component under test, or wrapped in a wrapper
142
- // component. We don't use decorators for the wrapper
143
- // because we may not be in a storybook context and it
144
- // keeps the framework API simpler this way.
145
-
90
+ const exportName = storyName.replace(/\b\w/g, c => c.toUpperCase()).replace(/[^\w]+/g, "").replace(/[_]+/g, "");
146
91
  let Template = templateMap.get(Component);
147
92
 
148
93
  if (Template == null) {
149
- // The MountingComponent is a bit different than just a
150
- // Storybook decorator. It's a React component that
151
- // takes over rendering the component in the fixture
152
- // with the given args, allowing for greater
153
- // customization in a platform-agnostic manner (i.e.
154
- // not just story format).
155
- Template = MountingComponent ? args => /*#__PURE__*/React.createElement(MountingComponent, {
94
+ Template = MountingComponent ? args => React.createElement(MountingComponent, {
156
95
  component: Component,
157
96
  props: args,
158
97
  log: log
159
- }) : args => /*#__PURE__*/React.createElement(Component, args);
98
+ }) : args => React.createElement(Component, args);
160
99
  templateMap.set(Component, Template);
161
- } // Each story that shares that component then reuses that
162
- // template.
163
-
100
+ }
164
101
 
165
102
  acc[exportName] = Template.bind({});
166
103
  acc[exportName].args = getProps({
167
104
  log
168
- }); // Adding a story name here means that we don't have to
169
- // care about naming the exports correctly, if we don't
170
- // want (useful if we need to autogenerate or manually
171
- // expose ESM exports).
172
-
105
+ });
173
106
  acc[exportName].storyName = storyName;
174
107
  return acc;
175
108
  }, {
@@ -186,20 +119,9 @@ var adapters = /*#__PURE__*/Object.freeze({
186
119
  });
187
120
 
188
121
  let _configuration = null;
189
- /**
190
- * Setup the fixture framework.
191
- */
192
-
193
122
  const setup = configuration => {
194
123
  _configuration = configuration;
195
124
  };
196
- /**
197
- * Get the framework configuration.
198
- *
199
- * @returns {Configuration} The configuration as provided via setup().
200
- * @throws {Error} If the configuration has not been set.
201
- */
202
-
203
125
  const getConfiguration = () => {
204
126
  if (_configuration == null) {
205
127
  throw new Error("Not configured");
@@ -208,26 +130,8 @@ const getConfiguration = () => {
208
130
  return _configuration;
209
131
  };
210
132
 
211
- /**
212
- * Combine two values.
213
- *
214
- * This method clones val2 before using any of its properties to try to ensure
215
- * the combined object is not linked back to the original.
216
- *
217
- * If the values are objects, it will merge them at the top level. Properties
218
- * themselves are not merged; val2 properties will overwrite val1 where there
219
- * are conflicts
220
- *
221
- * If the values are arrays, it will concatenate and dedupe them.
222
- * NOTE: duplicates in either val1 or val2 will also be deduped.
223
- *
224
- * If the values are any other type, or val2 has a different type to val1, val2
225
- * will be returned.
226
- */
227
-
228
133
  const combineTopLevel = (val1, val2) => {
229
- const obj2Clone = clone(val2); // Only merge if they're both arrays or both objects.
230
- // If not, we will just return val2.
134
+ const obj2Clone = clone(val2);
231
135
 
232
136
  if (val1 !== null && val2 !== null && typeof val1 === "object" && typeof val2 === "object") {
233
137
  const val1IsArray = Array.isArray(val1);
@@ -243,79 +147,26 @@ const combineTopLevel = (val1, val2) => {
243
147
  return obj2Clone;
244
148
  };
245
149
 
246
- /**
247
- * Combine one or more objects into a single object.
248
- *
249
- * Objects later in the argument list take precedence over those that are
250
- * earlier. Object and array values at the root level are merged.
251
- */
252
-
253
150
  const combineOptions = (...toBeCombined) => {
254
151
  const combined = toBeCombined.filter(Boolean).reduce((acc, cur) => {
255
152
  for (const key of Object.keys(cur)) {
256
- // We always call combine, even if acc[key] is undefined
257
- // because we need to make sure we clone values.
258
153
  acc[key] = combineTopLevel(acc[key], cur[key]);
259
154
  }
260
155
 
261
156
  return acc;
262
- }, {}); // We know that we are creating a compatible return type.
263
- // $FlowIgnore[incompatible-return]
264
-
157
+ }, {});
265
158
  return combined;
266
159
  };
267
160
 
268
161
  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") {
162
+ if (typeof componentOrOptions === "function" || typeof componentOrOptions.render === "function") {
289
163
  return {
290
- // $FlowIgnore[incompatible-return]
291
164
  component: componentOrOptions
292
165
  };
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
-
166
+ }
297
167
 
298
168
  return componentOrOptions;
299
169
  };
300
- /**
301
- * Describe a group of fixtures for a given component.
302
- *
303
- * Only one `fixtures` call should be used per fixture file as it returns
304
- * the exports for that file.
305
- *
306
- * @param {FixtureOptions<TProps>} options Options describing the
307
- * fixture group.
308
- * @param {FixtureFn<TProps> => void} fn A function that provides a `fixture`
309
- * function for defining fixtures.
310
- * @returns {Exports} The object to be exported as `module.exports`.
311
- *
312
- * TODO(somewhatabstract): Determine a way around this requirement so we
313
- * can support named exports and default exports via the adapters in a
314
- * deterministic way. Currently this is imposed on us because of how
315
- * storybook, the popular framework, uses both default and named exports for
316
- * its interface.
317
- */
318
-
319
170
 
320
171
  const fixtures = (componentOrOptions, fn) => {
321
172
  var _additionalAdapterOpt;
@@ -330,13 +181,12 @@ const fixtures = (componentOrOptions, fn) => {
330
181
  description: groupDescription,
331
182
  defaultWrapper,
332
183
  additionalAdapterOptions
333
- } = normalizeOptions(componentOrOptions); // 1. Create a new adapter group.
334
-
184
+ } = normalizeOptions(componentOrOptions);
335
185
  const group = adapter.declareGroup({
336
186
  title,
337
187
  description: groupDescription,
338
188
  getDefaultTitle: () => component.displayName || component.name || "Component"
339
- }); // 2. Invoke fn with a function that can add a new fixture.
189
+ });
340
190
 
341
191
  const addFixture = (description, props, wrapper = null) => {
342
192
  var _ref;
@@ -348,86 +198,38 @@ const fixtures = (componentOrOptions, fn) => {
348
198
  });
349
199
  };
350
200
 
351
- fn(addFixture); // 3. Combine the adapter options from the fixture group with the
352
- // defaults from our setup.
353
-
201
+ fn(addFixture);
354
202
  const groupAdapterOverrides = (_additionalAdapterOpt = additionalAdapterOptions == null ? void 0 : additionalAdapterOptions[adapter.name]) != null ? _additionalAdapterOpt : {};
355
- const combinedAdapterOptions = combineOptions(defaultAdapterOptions, groupAdapterOverrides); // 4. Call close on the group and return the result.
356
-
203
+ const combinedAdapterOptions = combineOptions(defaultAdapterOptions, groupAdapterOverrides);
357
204
  return group.closeGroup(combinedAdapterOptions);
358
205
  };
359
206
 
360
- // We need a version of Response. When we're in Jest JSDOM environment or a
361
- // version of Node that supports the fetch API (17 and up, possibly with
362
- // --experimental-fetch flag), then we're good, but otherwise we need an
363
- // implementation, so this uses node-fetch as a peer dependency and uses that
364
- // to provide the implementation if we don't already have one.
365
207
  const ResponseImpl = typeof Response === "undefined" ? require("node-fetch").Response : Response;
366
208
 
367
- /**
368
- * Helper for creating a text-based mock response.
369
- */
370
209
  const textResponse = (text, statusCode = 200) => ({
371
210
  type: "text",
372
211
  text,
373
212
  statusCode
374
213
  });
375
- /**
376
- * Helper for creating a rejected mock response.
377
- */
378
-
379
214
 
380
215
  const rejectResponse = error => ({
381
216
  type: "reject",
382
217
  error
383
218
  });
384
- /**
385
- * Helpers to define mock responses for mocked requests.
386
- */
387
-
388
219
 
389
220
  const RespondWith = Object.freeze({
390
- /**
391
- * Response with text body and status code.
392
- * Status code defaults to 200.
393
- */
394
221
  text: (text, statusCode = 200) => textResponse(text, statusCode),
395
-
396
- /**
397
- * Response with JSON body and status code 200.
398
- */
399
222
  json: json => textResponse(() => JSON.stringify(json)),
400
-
401
- /**
402
- * Response with GraphQL data JSON body and status code 200.
403
- */
404
223
  graphQLData: data => textResponse(() => JSON.stringify({
405
224
  data
406
225
  })),
407
-
408
- /**
409
- * Response with body that will not parse as JSON and status code 200.
410
- */
411
226
  unparseableBody: () => textResponse("INVALID JSON"),
412
-
413
- /**
414
- * Rejects with an AbortError to simulate an aborted request.
415
- */
416
227
  abortedRequest: () => rejectResponse(() => {
417
228
  const abortError = new Error("Mock request aborted");
418
229
  abortError.name = "AbortError";
419
230
  return abortError;
420
231
  }),
421
-
422
- /**
423
- * Rejects with the given error.
424
- */
425
232
  reject: error => rejectResponse(error),
426
-
427
- /**
428
- * A non-200 status code with empty text body.
429
- * Equivalent to calling `ResponseWith.text("", statusCode)`.
430
- */
431
233
  errorStatusCode: statusCode => {
432
234
  if (statusCode < 300) {
433
235
  throw new Error(`${statusCode} is not a valid error status code`);
@@ -435,28 +237,16 @@ const RespondWith = Object.freeze({
435
237
 
436
238
  return textResponse("{}", statusCode);
437
239
  },
438
-
439
- /**
440
- * Response body that is valid JSON but not a valid GraphQL response.
441
- */
442
240
  nonGraphQLBody: () => textResponse(() => JSON.stringify({
443
241
  valid: "json",
444
242
  that: "is not a valid graphql response"
445
243
  })),
446
-
447
- /**
448
- * Response that is a GraphQL errors response with status code 200.
449
- */
450
244
  graphQLErrors: errorMessages => textResponse(() => JSON.stringify({
451
245
  errors: errorMessages.map(e => ({
452
246
  message: e
453
247
  }))
454
248
  }))
455
249
  });
456
- /**
457
- * Turns a MockResponse value to an actual Response that represents the mock.
458
- */
459
-
460
250
  const makeMockResponse = response => {
461
251
  switch (response.type) {
462
252
  case "text":
@@ -474,13 +264,6 @@ const makeMockResponse = response => {
474
264
  }
475
265
  };
476
266
 
477
- /**
478
- * Get the URL from the given RequestInfo.
479
- *
480
- * Since we could be running in Node or in JSDOM, we don't check instance
481
- * types, but just use a heuristic so that this works without knowing what
482
- * was polyfilling things.
483
- */
484
267
  const getHref = input => {
485
268
  if (typeof input === "string") {
486
269
  return input;
@@ -492,15 +275,9 @@ const getHref = input => {
492
275
  throw new Error(`Unsupported input type`);
493
276
  }
494
277
  };
495
- /**
496
- * Determines if a given fetch invocation matches the given mock.
497
- */
498
-
499
278
 
500
279
  const fetchRequestMatchesMock = (mock, input, init) => {
501
- // Currently, we only match on the input portion.
502
- // This can be a Request, a URL, or a string.
503
- const href = getHref(input); // Our mock operation is either a string for an exact match, or a regex.
280
+ const href = getHref(input);
504
281
 
505
282
  if (typeof mock === "string") {
506
283
  return href === mock;
@@ -511,21 +288,12 @@ const fetchRequestMatchesMock = (mock, input, init) => {
511
288
  }
512
289
  };
513
290
 
514
- /**
515
- * A generic mock request function for using when mocking fetch or gqlFetch.
516
- */
517
291
  const mockRequester = (operationMatcher, operationToString) => {
518
- // We want this to work in jest and in fixtures to make life easy for folks.
519
- // This is the array of mocked operations that we will traverse and
520
- // manipulate.
521
- const mocks = []; // What we return has to be a drop in for the fetch function that is
522
- // provided to `GqlRouter` which is how folks will then use this mock.
292
+ const mocks = [];
523
293
 
524
294
  const mockFn = (...args) => {
525
- // Iterate our mocked operations and find the first one that matches.
526
295
  for (const mock of mocks) {
527
296
  if (mock.onceOnly && mock.used) {
528
- // This is a once-only mock and it has been used, so skip it.
529
297
  continue;
530
298
  }
531
299
 
@@ -533,9 +301,7 @@ const mockRequester = (operationMatcher, operationToString) => {
533
301
  mock.used = true;
534
302
  return mock.response();
535
303
  }
536
- } // Default is to reject with some helpful info on what request
537
- // we rejected.
538
-
304
+ }
539
305
 
540
306
  return Promise.reject(new Error(`No matching mock response found for request:
541
307
  ${operationToString.apply(void 0, args)}`));
@@ -560,19 +326,10 @@ const mockRequester = (operationMatcher, operationToString) => {
560
326
  return mockFn;
561
327
  };
562
328
 
563
- /**
564
- * A mock for the fetch function passed to GqlRouter.
565
- */
566
329
  const mockFetch = () => mockRequester(fetchRequestMatchesMock, (input, init) => `Input: ${typeof input === "string" ? input : JSON.stringify(input, null, 2)}
567
330
  Options: ${init == null ? "None" : JSON.stringify(init, null, 2)}`);
568
331
 
569
- const safeHasOwnProperty = (obj, prop) => // Flow really shouldn't be raising this error here.
570
- // $FlowFixMe[method-unbinding]
571
- Object.prototype.hasOwnProperty.call(obj, prop); // TODO(somewhatabstract, FEI-4268): use a third-party library to do this and
572
- // possibly make it also support the jest `jest.objectContaining` type matching
573
- // to simplify mock declaration (note that it would need to work in regular
574
- // tests and stories/fixtures).
575
-
332
+ const safeHasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
576
333
 
577
334
  const areObjectsEqual = (a, b) => {
578
335
  if (a === b) {
@@ -606,38 +363,25 @@ const areObjectsEqual = (a, b) => {
606
363
  };
607
364
 
608
365
  const gqlRequestMatchesMock = (mock, operation, variables, context) => {
609
- // If they don't represent the same operation, then they can't match.
610
- // NOTE: Operations can include more fields than id and type, but we only
611
- // care about id and type. The rest is ignored.
612
366
  if (mock.operation.id !== operation.id || mock.operation.type !== operation.type) {
613
367
  return false;
614
- } // We do a loose match, so if the lhs doesn't define variables,
615
- // we just assume it matches everything.
616
-
368
+ }
617
369
 
618
370
  if (mock.variables != null) {
619
- // Variables have to match.
620
371
  if (!areObjectsEqual(mock.variables, variables)) {
621
372
  return false;
622
373
  }
623
- } // We do a loose match, so if the lhs doesn't define context,
624
- // we just assume it matches everything.
625
-
374
+ }
626
375
 
627
376
  if (mock.context != null) {
628
- // Context has to match.
629
377
  if (!areObjectsEqual(mock.context, context)) {
630
378
  return false;
631
379
  }
632
- } // If we get here, we have a match.
633
-
380
+ }
634
381
 
635
382
  return true;
636
383
  };
637
384
 
638
- /**
639
- * A mock for the fetch function passed to GqlRouter.
640
- */
641
385
  const mockGqlFetch = () => mockRequester(gqlRequestMatchesMock, (operation, variables, context) => `Operation: ${operation.type} ${operation.id}
642
386
  Variables: ${variables == null ? "None" : JSON.stringify(variables, null, 2)}
643
387
  Context: ${JSON.stringify(context, null, 2)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-testing",
3
- "version": "4.0.0",
3
+ "version": "4.0.3",
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": "^7.0.0"
17
+ "@khanacademy/wonder-blocks-data": "^8.0.1"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@khanacademy/wonder-stuff-core": "^0.1.2",
@@ -24,7 +24,7 @@
24
24
  "react": "16.14.0"
25
25
  },
26
26
  "devDependencies": {
27
- "wb-dev-build-settings": "^0.3.0"
27
+ "wb-dev-build-settings": "^0.4.0"
28
28
  },
29
29
  "author": "",
30
30
  "license": "MIT"