@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 +39 -2
- package/dist/es/index.js +200 -24
- package/dist/index.js +249 -52
- package/package.json +6 -4
- package/src/fixtures/__tests__/setup.test.js +6 -4
- package/src/gql/__tests__/gql-request-matches-mock.test.js +235 -0
- package/src/gql/__tests__/make-gql-mock-response.test.js +298 -0
- package/src/gql/__tests__/mock-gql-fetch.test.js +469 -0
- package/src/gql/__tests__/wb-data-integration.test.js +267 -0
- package/src/gql/gql-request-matches-mock.js +74 -0
- package/src/gql/make-gql-mock-response.js +124 -0
- package/src/gql/mock-gql-fetch.js +89 -0
- package/src/gql/types.js +35 -0
- package/src/index.js +17 -3
- package/src/jest/isolate-modules.js +0 -31
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
|
-
-
|
|
7
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// We
|
|
348
|
-
//
|
|
349
|
-
//
|
|
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
|
|
527
|
+
return gqlFetchMock;
|
|
352
528
|
};
|
|
353
529
|
|
|
354
|
-
export { adapters, fixtures,
|
|
530
|
+
export { RespondWith, adapters, fixtures, mockGqlFetch, setup as setupFixtures };
|