@khanacademy/wonder-blocks-testing 1.0.0 → 2.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
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 274caaac: Remove isolateModules (now implemented by @khanacademy/wonder-stuff-testing), export GQL framework, export fixture framework types
8
+
9
+ ### Patch Changes
10
+
11
+ - @khanacademy/wonder-blocks-data@3.1.2
12
+
3
13
  ## 1.0.0
4
14
 
5
15
  ### Major Changes
package/dist/es/index.js CHANGED
@@ -319,36 +319,212 @@ const fixtures = (options, fn) => {
319
319
  return group.closeGroup(combinedAdapterOptions);
320
320
  };
321
321
 
322
- // Opt this file out of coverage because it's super hard to test.
322
+ const safeHasOwnProperty = (obj, prop) => // Flow really shouldn't be raising this error here.
323
+ // $FlowFixMe[method-unbinding]
324
+ Object.prototype.hasOwnProperty.call(obj, prop); // TODO(somewhatabstract, FEI-4268): use a third-party library to do this and
325
+ // possibly make it also support the jest `jest.objectContaining` type matching
326
+ // to simplify mock declaration (note that it would need to work in regular
327
+ // tests and stories/fixtures).
323
328
 
324
- /* istanbul ignore file */
329
+
330
+ const areObjectsEqual = (a, b) => {
331
+ if (a === b) {
332
+ return true;
333
+ }
334
+
335
+ if (a == null || b == null) {
336
+ return false;
337
+ }
338
+
339
+ if (typeof a !== "object" || typeof b !== "object") {
340
+ return false;
341
+ }
342
+
343
+ const aKeys = Object.keys(a);
344
+ const bKeys = Object.keys(b);
345
+
346
+ if (aKeys.length !== bKeys.length) {
347
+ return false;
348
+ }
349
+
350
+ for (let i = 0; i < aKeys.length; i++) {
351
+ const key = aKeys[i];
352
+
353
+ if (!safeHasOwnProperty(b, key) || !areObjectsEqual(a[key], b[key])) {
354
+ return false;
355
+ }
356
+ }
357
+
358
+ return true;
359
+ };
360
+
361
+ const gqlRequestMatchesMock = (mock, operation, variables, context) => {
362
+ // If they don't represent the same operation, then they can't match.
363
+ // NOTE: Operations can include more fields than id and type, but we only
364
+ // care about id and type. The rest is ignored.
365
+ if (mock.operation.id !== operation.id || mock.operation.type !== operation.type) {
366
+ return false;
367
+ } // We do a loose match, so if the lhs doesn't define variables,
368
+ // we just assume it matches everything.
369
+
370
+
371
+ if (mock.variables != null) {
372
+ // Variables have to match.
373
+ if (!areObjectsEqual(mock.variables, variables)) {
374
+ return false;
375
+ }
376
+ } // We do a loose match, so if the lhs doesn't define context,
377
+ // we just assume it matches everything.
378
+
379
+
380
+ if (mock.context != null) {
381
+ // Context has to match.
382
+ if (!areObjectsEqual(mock.context, context)) {
383
+ return false;
384
+ }
385
+ } // If we get here, we have a match.
386
+
387
+
388
+ return true;
389
+ };
325
390
 
326
391
  /**
327
- * Isolate imports within a given action using jest.isolateModules.
328
- *
329
- * This is a helper for the `jest.isolateModules` API, allowing
330
- * code to avoid the clunky closure syntax in their tests.
331
- *
332
- * @param {() => T} action The action that contains the isolated module imports.
333
- * We do it this way so that any `require` calls are relative to the calling
334
- * code and not this function. Note that we don't support promises here to
335
- * discourage dynamic `import` use, which doesn't play well with standard
336
- * jest yet.
392
+ * Helpers to define rejection states for mocking GQL requests.
337
393
  */
338
- const isolateModules = action => {
339
- if (typeof jest === "undefined") {
340
- throw new Error(`jest is not available in global scope`);
394
+ const RespondWith = Object.freeze({
395
+ data: data => ({
396
+ type: "data",
397
+ data
398
+ }),
399
+ unparseableBody: () => ({
400
+ type: "parse"
401
+ }),
402
+ abortedRequest: () => ({
403
+ type: "abort"
404
+ }),
405
+ errorStatusCode: statusCode => {
406
+ if (statusCode < 300) {
407
+ throw new Error(`${statusCode} is not a valid error status code`);
408
+ }
409
+
410
+ return {
411
+ type: "status",
412
+ statusCode
413
+ };
414
+ },
415
+ nonGraphQLBody: () => ({
416
+ type: "invalid"
417
+ }),
418
+ graphQLErrors: errorMessages => ({
419
+ type: "graphql",
420
+ errors: errorMessages
421
+ })
422
+ });
423
+ /**
424
+ * Turns an ErrorResponse value in an actual Response that will invoke
425
+ * that error.
426
+ */
427
+
428
+ const makeGqlMockResponse = response => {
429
+ switch (response.type) {
430
+ case "data":
431
+ return Promise.resolve({
432
+ status: 200,
433
+ text: () => Promise.resolve(JSON.stringify({
434
+ data: response.data
435
+ }))
436
+ });
437
+
438
+ case "parse":
439
+ return Promise.resolve({
440
+ status: 200,
441
+ text: () => Promise.resolve("INVALID JSON")
442
+ });
443
+
444
+ case "abort":
445
+ const abortError = new Error("Mock request aborted");
446
+ abortError.name = "AbortError";
447
+ return Promise.reject(abortError);
448
+
449
+ case "status":
450
+ return Promise.resolve({
451
+ status: response.statusCode,
452
+ text: () => Promise.resolve(JSON.stringify({}))
453
+ });
454
+
455
+ case "invalid":
456
+ return Promise.resolve({
457
+ status: 200,
458
+ text: () => Promise.resolve(JSON.stringify({
459
+ valid: "json",
460
+ that: "is not a valid graphql response"
461
+ }))
462
+ });
463
+
464
+ case "graphql":
465
+ return Promise.resolve({
466
+ status: 200,
467
+ text: () => Promise.resolve(JSON.stringify({
468
+ errors: response.errors.map(e => ({
469
+ message: e
470
+ }))
471
+ }))
472
+ });
473
+
474
+ default:
475
+ throw new Error(`Unknown response type: ${response.type}`);
341
476
  }
477
+ };
342
478
 
343
- let result = undefined;
344
- jest.isolateModules(() => {
345
- result = action();
346
- }); // We know that we'll have a result of the appropriate type at this point.
347
- // We could use a promise to make everything happy, but this doesn't need
348
- // to be async, so why bother.
349
- // $FlowIgnore[incompatible-return]
479
+ /**
480
+ * A mock for the fetch function passed to GqlRouter.
481
+ */
482
+ const mockGqlFetch = () => {
483
+ // We want this to work in jest and in fixtures to make life easy for folks.
484
+ // This is the array of mocked operations that we will traverse and
485
+ // manipulate.
486
+ const mocks = []; // What we return has to be a drop in for the fetch function that is
487
+ // provided to `GqlRouter` which is how folks will then use this mock.
488
+
489
+ const gqlFetchMock = (operation, variables, context) => {
490
+ // Iterate our mocked operations and find the first one that matches.
491
+ for (const mock of mocks) {
492
+ if (mock.onceOnly && mock.used) {
493
+ // This is a once-only mock and it has been used, so skip it.
494
+ continue;
495
+ }
496
+
497
+ if (gqlRequestMatchesMock(mock.operation, operation, variables, context)) {
498
+ mock.used = true;
499
+ return mock.response();
500
+ }
501
+ } // Default is to reject with some helpful info on what request
502
+ // we rejected.
503
+
504
+
505
+ return Promise.reject(new Error(`No matching GraphQL mock response found for request:
506
+ Operation: ${operation.type} ${operation.id}
507
+ Variables: ${variables == null ? "None" : JSON.stringify(variables, null, 2)}
508
+ Context: ${JSON.stringify(context, null, 2)}`));
509
+ };
510
+
511
+ const addMockedOperation = (operation, response, onceOnly) => {
512
+ const mockResponse = () => makeGqlMockResponse(response);
513
+
514
+ mocks.push({
515
+ operation,
516
+ response: mockResponse,
517
+ onceOnly,
518
+ used: false
519
+ });
520
+ return gqlFetchMock;
521
+ };
522
+
523
+ gqlFetchMock.mockOperation = (operation, response) => addMockedOperation(operation, response, false);
524
+
525
+ gqlFetchMock.mockOperationOnce = (operation, response) => addMockedOperation(operation, response, true);
350
526
 
351
- return result;
527
+ return gqlFetchMock;
352
528
  };
353
529
 
354
- export { adapters, fixtures, isolateModules, setup as setupFixtures };
530
+ export { RespondWith, adapters, fixtures, mockGqlFetch, setup as setupFixtures };
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ module.exports =
82
82
  /******/
83
83
  /******/
84
84
  /******/ // Load entry module and return exports
85
- /******/ return __webpack_require__(__webpack_require__.s = 12);
85
+ /******/ return __webpack_require__(__webpack_require__.s = 14);
86
86
  /******/ })
87
87
  /************************************************************************/
88
88
  /******/ ([
@@ -117,32 +117,127 @@ const getConfiguration = () => {
117
117
 
118
118
  /***/ }),
119
119
  /* 1 */
120
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
121
+
122
+ "use strict";
123
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return RespondWith; });
124
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return makeGqlMockResponse; });
125
+ /**
126
+ * Helpers to define rejection states for mocking GQL requests.
127
+ */
128
+ const RespondWith = Object.freeze({
129
+ data: data => ({
130
+ type: "data",
131
+ data
132
+ }),
133
+ unparseableBody: () => ({
134
+ type: "parse"
135
+ }),
136
+ abortedRequest: () => ({
137
+ type: "abort"
138
+ }),
139
+ errorStatusCode: statusCode => {
140
+ if (statusCode < 300) {
141
+ throw new Error(`${statusCode} is not a valid error status code`);
142
+ }
143
+
144
+ return {
145
+ type: "status",
146
+ statusCode
147
+ };
148
+ },
149
+ nonGraphQLBody: () => ({
150
+ type: "invalid"
151
+ }),
152
+ graphQLErrors: errorMessages => ({
153
+ type: "graphql",
154
+ errors: errorMessages
155
+ })
156
+ });
157
+ /**
158
+ * Turns an ErrorResponse value in an actual Response that will invoke
159
+ * that error.
160
+ */
161
+
162
+ const makeGqlMockResponse = response => {
163
+ switch (response.type) {
164
+ case "data":
165
+ return Promise.resolve({
166
+ status: 200,
167
+ text: () => Promise.resolve(JSON.stringify({
168
+ data: response.data
169
+ }))
170
+ });
171
+
172
+ case "parse":
173
+ return Promise.resolve({
174
+ status: 200,
175
+ text: () => Promise.resolve("INVALID JSON")
176
+ });
177
+
178
+ case "abort":
179
+ const abortError = new Error("Mock request aborted");
180
+ abortError.name = "AbortError";
181
+ return Promise.reject(abortError);
182
+
183
+ case "status":
184
+ return Promise.resolve({
185
+ status: response.statusCode,
186
+ text: () => Promise.resolve(JSON.stringify({}))
187
+ });
188
+
189
+ case "invalid":
190
+ return Promise.resolve({
191
+ status: 200,
192
+ text: () => Promise.resolve(JSON.stringify({
193
+ valid: "json",
194
+ that: "is not a valid graphql response"
195
+ }))
196
+ });
197
+
198
+ case "graphql":
199
+ return Promise.resolve({
200
+ status: 200,
201
+ text: () => Promise.resolve(JSON.stringify({
202
+ errors: response.errors.map(e => ({
203
+ message: e
204
+ }))
205
+ }))
206
+ });
207
+
208
+ default:
209
+ throw new Error(`Unknown response type: ${response.type}`);
210
+ }
211
+ };
212
+
213
+ /***/ }),
214
+ /* 2 */
120
215
  /***/ (function(module, exports) {
121
216
 
122
217
  module.exports = require("react");
123
218
 
124
219
  /***/ }),
125
- /* 2 */
220
+ /* 3 */
126
221
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
127
222
 
128
223
  "use strict";
129
224
  __webpack_require__.r(__webpack_exports__);
130
- /* harmony import */ var _storybook_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
225
+ /* harmony import */ var _storybook_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
131
226
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "storybook", function() { return _storybook_js__WEBPACK_IMPORTED_MODULE_0__["a"]; });
132
227
 
133
228
 
134
229
 
135
230
  /***/ }),
136
- /* 3 */
231
+ /* 4 */
137
232
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
138
233
 
139
234
  "use strict";
140
235
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return getAdapter; });
141
- /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
236
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
142
237
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
143
- /* harmony import */ var _storybook_addon_actions__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
238
+ /* harmony import */ var _storybook_addon_actions__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
144
239
  /* harmony import */ var _storybook_addon_actions__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_storybook_addon_actions__WEBPACK_IMPORTED_MODULE_1__);
145
- /* harmony import */ var _adapter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
240
+ /* harmony import */ var _adapter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
146
241
 
147
242
 
148
243
 
@@ -215,15 +310,15 @@ const getAdapter = (MountingComponent = null) => new _adapter_js__WEBPACK_IMPORT
215
310
  });
216
311
 
217
312
  /***/ }),
218
- /* 4 */
313
+ /* 5 */
219
314
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
220
315
 
221
316
  "use strict";
222
317
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return fixtures; });
223
- /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
318
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
224
319
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
225
320
  /* harmony import */ var _setup_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
226
- /* harmony import */ var _combine_options_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
321
+ /* harmony import */ var _combine_options_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
227
322
 
228
323
 
229
324
 
@@ -286,56 +381,80 @@ const fixtures = (options, fn) => {
286
381
  };
287
382
 
288
383
  /***/ }),
289
- /* 5 */
384
+ /* 6 */
290
385
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
291
386
 
292
387
  "use strict";
293
- /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return isolateModules; });
294
- // Opt this file out of coverage because it's super hard to test.
388
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return mockGqlFetch; });
389
+ /* harmony import */ var _gql_request_matches_mock_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
390
+ /* harmony import */ var _make_gql_mock_response_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1);
391
+
295
392
 
296
- /* istanbul ignore file */
297
393
 
298
394
  /**
299
- * Isolate imports within a given action using jest.isolateModules.
300
- *
301
- * This is a helper for the `jest.isolateModules` API, allowing
302
- * code to avoid the clunky closure syntax in their tests.
303
- *
304
- * @param {() => T} action The action that contains the isolated module imports.
305
- * We do it this way so that any `require` calls are relative to the calling
306
- * code and not this function. Note that we don't support promises here to
307
- * discourage dynamic `import` use, which doesn't play well with standard
308
- * jest yet.
395
+ * A mock for the fetch function passed to GqlRouter.
309
396
  */
310
- const isolateModules = action => {
311
- if (typeof jest === "undefined") {
312
- throw new Error(`jest is not available in global scope`);
313
- }
397
+ const mockGqlFetch = () => {
398
+ // We want this to work in jest and in fixtures to make life easy for folks.
399
+ // This is the array of mocked operations that we will traverse and
400
+ // manipulate.
401
+ const mocks = []; // What we return has to be a drop in for the fetch function that is
402
+ // provided to `GqlRouter` which is how folks will then use this mock.
403
+
404
+ const gqlFetchMock = (operation, variables, context) => {
405
+ // Iterate our mocked operations and find the first one that matches.
406
+ for (const mock of mocks) {
407
+ if (mock.onceOnly && mock.used) {
408
+ // This is a once-only mock and it has been used, so skip it.
409
+ continue;
410
+ }
411
+
412
+ if (Object(_gql_request_matches_mock_js__WEBPACK_IMPORTED_MODULE_0__[/* gqlRequestMatchesMock */ "a"])(mock.operation, operation, variables, context)) {
413
+ mock.used = true;
414
+ return mock.response();
415
+ }
416
+ } // Default is to reject with some helpful info on what request
417
+ // we rejected.
314
418
 
315
- let result = undefined;
316
- jest.isolateModules(() => {
317
- result = action();
318
- }); // We know that we'll have a result of the appropriate type at this point.
319
- // We could use a promise to make everything happy, but this doesn't need
320
- // to be async, so why bother.
321
- // $FlowIgnore[incompatible-return]
322
419
 
323
- return result;
420
+ return Promise.reject(new Error(`No matching GraphQL mock response found for request:
421
+ Operation: ${operation.type} ${operation.id}
422
+ Variables: ${variables == null ? "None" : JSON.stringify(variables, null, 2)}
423
+ Context: ${JSON.stringify(context, null, 2)}`));
424
+ };
425
+
426
+ const addMockedOperation = (operation, response, onceOnly) => {
427
+ const mockResponse = () => Object(_make_gql_mock_response_js__WEBPACK_IMPORTED_MODULE_1__[/* makeGqlMockResponse */ "b"])(response);
428
+
429
+ mocks.push({
430
+ operation,
431
+ response: mockResponse,
432
+ onceOnly,
433
+ used: false
434
+ });
435
+ return gqlFetchMock;
436
+ };
437
+
438
+ gqlFetchMock.mockOperation = (operation, response) => addMockedOperation(operation, response, false);
439
+
440
+ gqlFetchMock.mockOperationOnce = (operation, response) => addMockedOperation(operation, response, true);
441
+
442
+ return gqlFetchMock;
324
443
  };
325
444
 
326
445
  /***/ }),
327
- /* 6 */
446
+ /* 7 */
328
447
  /***/ (function(module, exports) {
329
448
 
330
449
  module.exports = require("@storybook/addon-actions");
331
450
 
332
451
  /***/ }),
333
- /* 7 */
452
+ /* 8 */
334
453
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
335
454
 
336
455
  "use strict";
337
456
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Adapter; });
338
- /* harmony import */ var _adapter_group_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8);
457
+ /* harmony import */ var _adapter_group_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
339
458
 
340
459
 
341
460
  /**
@@ -390,7 +509,7 @@ class Adapter {
390
509
  }
391
510
 
392
511
  /***/ }),
393
- /* 8 */
512
+ /* 9 */
394
513
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
395
514
 
396
515
  "use strict";
@@ -455,12 +574,12 @@ class AdapterGroup {
455
574
  }
456
575
 
457
576
  /***/ }),
458
- /* 9 */
577
+ /* 10 */
459
578
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
460
579
 
461
580
  "use strict";
462
581
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return combineOptions; });
463
- /* harmony import */ var _combine_top_level_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(10);
582
+ /* harmony import */ var _combine_top_level_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(11);
464
583
 
465
584
  /**
466
585
  * Combine one or more objects into a single object.
@@ -485,12 +604,12 @@ const combineOptions = (...toBeCombined) => {
485
604
  };
486
605
 
487
606
  /***/ }),
488
- /* 10 */
607
+ /* 11 */
489
608
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
490
609
 
491
610
  "use strict";
492
611
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return combineTopLevel; });
493
- /* harmony import */ var _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(11);
612
+ /* harmony import */ var _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12);
494
613
  /* harmony import */ var _khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_stuff_core__WEBPACK_IMPORTED_MODULE_0__);
495
614
 
496
615
  /**
@@ -531,34 +650,112 @@ const combineTopLevel = (val1, val2) => {
531
650
  };
532
651
 
533
652
  /***/ }),
534
- /* 11 */
653
+ /* 12 */
535
654
  /***/ (function(module, exports) {
536
655
 
537
656
  module.exports = require("@khanacademy/wonder-stuff-core");
538
657
 
539
658
  /***/ }),
540
- /* 12 */
659
+ /* 13 */
660
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
661
+
662
+ "use strict";
663
+ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return gqlRequestMatchesMock; });
664
+ const safeHasOwnProperty = (obj, prop) => // Flow really shouldn't be raising this error here.
665
+ // $FlowFixMe[method-unbinding]
666
+ Object.prototype.hasOwnProperty.call(obj, prop); // TODO(somewhatabstract, FEI-4268): use a third-party library to do this and
667
+ // possibly make it also support the jest `jest.objectContaining` type matching
668
+ // to simplify mock declaration (note that it would need to work in regular
669
+ // tests and stories/fixtures).
670
+
671
+
672
+ const areObjectsEqual = (a, b) => {
673
+ if (a === b) {
674
+ return true;
675
+ }
676
+
677
+ if (a == null || b == null) {
678
+ return false;
679
+ }
680
+
681
+ if (typeof a !== "object" || typeof b !== "object") {
682
+ return false;
683
+ }
684
+
685
+ const aKeys = Object.keys(a);
686
+ const bKeys = Object.keys(b);
687
+
688
+ if (aKeys.length !== bKeys.length) {
689
+ return false;
690
+ }
691
+
692
+ for (let i = 0; i < aKeys.length; i++) {
693
+ const key = aKeys[i];
694
+
695
+ if (!safeHasOwnProperty(b, key) || !areObjectsEqual(a[key], b[key])) {
696
+ return false;
697
+ }
698
+ }
699
+
700
+ return true;
701
+ };
702
+
703
+ const gqlRequestMatchesMock = (mock, operation, variables, context) => {
704
+ // If they don't represent the same operation, then they can't match.
705
+ // NOTE: Operations can include more fields than id and type, but we only
706
+ // care about id and type. The rest is ignored.
707
+ if (mock.operation.id !== operation.id || mock.operation.type !== operation.type) {
708
+ return false;
709
+ } // We do a loose match, so if the lhs doesn't define variables,
710
+ // we just assume it matches everything.
711
+
712
+
713
+ if (mock.variables != null) {
714
+ // Variables have to match.
715
+ if (!areObjectsEqual(mock.variables, variables)) {
716
+ return false;
717
+ }
718
+ } // We do a loose match, so if the lhs doesn't define context,
719
+ // we just assume it matches everything.
720
+
721
+
722
+ if (mock.context != null) {
723
+ // Context has to match.
724
+ if (!areObjectsEqual(mock.context, context)) {
725
+ return false;
726
+ }
727
+ } // If we get here, we have a match.
728
+
729
+
730
+ return true;
731
+ };
732
+
733
+ /***/ }),
734
+ /* 14 */
541
735
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
542
736
 
543
737
  "use strict";
544
738
  __webpack_require__.r(__webpack_exports__);
545
- /* harmony import */ var _fixtures_adapters_adapters_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
739
+ /* harmony import */ var _fixtures_adapters_adapters_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
546
740
  /* harmony reexport (module object) */ __webpack_require__.d(__webpack_exports__, "adapters", function() { return _fixtures_adapters_adapters_js__WEBPACK_IMPORTED_MODULE_0__; });
547
- /* harmony import */ var _fixtures_fixtures_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
741
+ /* harmony import */ var _fixtures_fixtures_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
548
742
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fixtures", function() { return _fixtures_fixtures_js__WEBPACK_IMPORTED_MODULE_1__["a"]; });
549
743
 
550
744
  /* harmony import */ var _fixtures_setup_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(0);
551
745
  /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "setupFixtures", function() { return _fixtures_setup_js__WEBPACK_IMPORTED_MODULE_2__["b"]; });
552
746
 
553
- /* harmony import */ var _jest_isolate_modules_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5);
554
- /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isolateModules", function() { return _jest_isolate_modules_js__WEBPACK_IMPORTED_MODULE_3__["a"]; });
747
+ /* harmony import */ var _gql_mock_gql_fetch_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
748
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mockGqlFetch", function() { return _gql_mock_gql_fetch_js__WEBPACK_IMPORTED_MODULE_3__["a"]; });
749
+
750
+ /* harmony import */ var _gql_make_gql_mock_response_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(1);
751
+ /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "RespondWith", function() { return _gql_make_gql_mock_response_js__WEBPACK_IMPORTED_MODULE_4__["a"]; });
555
752
 
556
753
  // Fixtures framework
557
754
 
558
755
 
559
756
 
560
- // Jest
561
- // TODO(somewhatabstract): To be moved to wonder stuff
757
+
758
+ // GraphQL framework
562
759
 
563
760
 
564
761
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-testing",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,15 +14,16 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@babel/runtime": "^7.16.3",
17
- "@khanacademy/wonder-blocks-data": "^3.1.1"
17
+ "@khanacademy/wonder-blocks-data": "^3.1.2"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@khanacademy/wonder-stuff-core": "^0.1.2",
21
+ "@khanacademy/wonder-stuff-testing": "^0.0.2",
21
22
  "@storybook/addon-actions": "^6.4.8",
22
23
  "react": "16.14.0"
23
24
  },
24
25
  "devDependencies": {
25
- "wb-dev-build-settings": "^0.2.0"
26
+ "wb-dev-build-settings": "^0.3.0"
26
27
  },
27
28
  "author": "",
28
29
  "license": "MIT"
@@ -1,10 +1,10 @@
1
1
  // @flow
2
- import {isolateModules} from "../../jest/isolate-modules.js";
2
+ import {jest as wst} from "@khanacademy/wonder-stuff-testing";
3
3
 
4
4
  describe("#getConfiguration", () => {
5
5
  it("should return the configuration passed during setup", () => {
6
6
  // Arrange
7
- const {setup, getConfiguration} = isolateModules(() =>
7
+ const {setup, getConfiguration} = wst.isolateModules(() =>
8
8
  require("../setup.js"),
9
9
  );
10
10
  const configuration = {
@@ -25,7 +25,9 @@ describe("#getConfiguration", () => {
25
25
 
26
26
  it("should throw if setup has not been performed", () => {
27
27
  // Arrange
28
- const {getConfiguration} = isolateModules(() => require("../setup.js"));
28
+ const {getConfiguration} = wst.isolateModules(() =>
29
+ require("../setup.js"),
30
+ );
29
31
 
30
32
  // Act
31
33
  const underTest = () => getConfiguration();
@@ -40,7 +42,7 @@ describe("#getConfiguration", () => {
40
42
  describe("#setup", () => {
41
43
  it("should set the configuration returned by getConfiguration", () => {
42
44
  // Arrange
43
- const {setup, getConfiguration} = isolateModules(() =>
45
+ const {setup, getConfiguration} = wst.isolateModules(() =>
44
46
  require("../setup.js"),
45
47
  );
46
48
  const configuration1 = {
package/src/index.js CHANGED
@@ -4,7 +4,21 @@
4
4
  export * as adapters from "./fixtures/adapters/adapters.js";
5
5
  export {fixtures} from "./fixtures/fixtures.js";
6
6
  export {setup as setupFixtures} from "./fixtures/setup.js";
7
+ export type {
8
+ Adapter,
9
+ AdapterFactory,
10
+ AdapterFixtureOptions,
11
+ AdapterGroup,
12
+ AdapterGroupOptions,
13
+ AdapterOptions,
14
+ Configuration,
15
+ CustomWrapperProps,
16
+ GetPropsOptions,
17
+ FixturesOptions,
18
+ } from "./fixtures/types.js";
7
19
 
8
- // Jest
9
- // TODO(somewhatabstract): To be moved to wonder stuff
10
- export {isolateModules} from "./jest/isolate-modules.js";
20
+ // GraphQL framework
21
+ export {mockGqlFetch} from "./gql/mock-gql-fetch.js";
22
+ export type {GqlMockResponse} from "./gql/make-gql-mock-response.js";
23
+ export {RespondWith} from "./gql/make-gql-mock-response.js";
24
+ export type {GqlFetchMockFn, GqlMock, GqlMockOperation} from "./gql/types.js";
@@ -1,31 +0,0 @@
1
- // @flow
2
- // Opt this file out of coverage because it's super hard to test.
3
- /* istanbul ignore file */
4
-
5
- /**
6
- * Isolate imports within a given action using jest.isolateModules.
7
- *
8
- * This is a helper for the `jest.isolateModules` API, allowing
9
- * code to avoid the clunky closure syntax in their tests.
10
- *
11
- * @param {() => T} action The action that contains the isolated module imports.
12
- * We do it this way so that any `require` calls are relative to the calling
13
- * code and not this function. Note that we don't support promises here to
14
- * discourage dynamic `import` use, which doesn't play well with standard
15
- * jest yet.
16
- */
17
- export const isolateModules = <T>(action: () => T): T => {
18
- if (typeof jest === "undefined") {
19
- throw new Error(`jest is not available in global scope`);
20
- }
21
-
22
- let result = undefined;
23
- jest.isolateModules(() => {
24
- result = action();
25
- });
26
- // We know that we'll have a result of the appropriate type at this point.
27
- // We could use a promise to make everything happy, but this doesn't need
28
- // to be async, so why bother.
29
- // $FlowIgnore[incompatible-return]
30
- return result;
31
- };