@khanacademy/wonder-blocks-testing 0.0.2 → 2.0.2

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,7 +1,44 @@
1
1
  # @khanacademy/wonder-blocks-testing
2
2
 
3
+ ## 2.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6973afa2]
8
+ - @khanacademy/wonder-blocks-data@3.2.0
9
+
10
+ ## 2.0.1
11
+
12
+ ### Patch Changes
13
+
14
+ - 9931ae6b: Simplify GQL types
15
+ - Updated dependencies [9931ae6b]
16
+ - @khanacademy/wonder-blocks-data@3.1.3
17
+
18
+ ## 2.0.0
19
+
20
+ ### Major Changes
21
+
22
+ - 274caaac: Remove isolateModules (now implemented by @khanacademy/wonder-stuff-testing), export GQL framework, export fixture framework types
23
+
24
+ ### Patch Changes
25
+
26
+ - @khanacademy/wonder-blocks-data@3.1.2
27
+
28
+ ## 1.0.0
29
+
30
+ ### Major Changes
31
+
32
+ - 4ff59815: Add GraphQL fetch mock support to wonder-blocks-testing
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies [4ff59815]
37
+ - @khanacademy/wonder-blocks-data@3.1.1
38
+
3
39
  ## 0.0.2
40
+
4
41
  ### Patch Changes
5
42
 
6
- - d2dba67a: Implemented the fixture framework and added the storybook adapter for it
7
- - b7a100f2: Add the new wonder-blocks-testing package
43
+ - d2dba67a: Implemented the fixture framework and added the storybook adapter for it
44
+ - b7a100f2: Add the new wonder-blocks-testing package
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 };