@khanacademy/wonder-blocks-data 3.1.1 → 4.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +375 -335
  3. package/dist/index.js +527 -461
  4. package/docs.md +17 -35
  5. package/package.json +3 -3
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +56 -122
  8. package/src/components/__tests__/data.test.js +372 -297
  9. package/src/components/__tests__/intercept-data.test.js +6 -30
  10. package/src/components/data.js +153 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/gql-router.js +1 -1
  13. package/src/components/intercept-context.js +6 -2
  14. package/src/components/intercept-data.js +40 -51
  15. package/src/components/intercept-data.md +13 -27
  16. package/src/components/track-data.md +9 -23
  17. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  18. package/src/hooks/__tests__/use-gql.test.js +1 -0
  19. package/src/hooks/__tests__/use-server-effect.test.js +217 -0
  20. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  21. package/src/hooks/use-gql.js +39 -31
  22. package/src/hooks/use-server-effect.js +45 -0
  23. package/src/hooks/use-shared-cache.js +106 -0
  24. package/src/index.js +15 -19
  25. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  26. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  27. package/src/util/__tests__/request-tracking.test.js +72 -191
  28. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  29. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  30. package/src/util/__tests__/ssr-cache.test.js +639 -0
  31. package/src/util/gql-types.js +5 -10
  32. package/src/util/request-fulfillment.js +36 -44
  33. package/src/util/request-tracking.js +62 -75
  34. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  35. package/src/util/scoped-in-memory-cache.js +149 -0
  36. package/src/util/ssr-cache.js +206 -0
  37. package/src/util/types.js +43 -108
  38. package/src/hooks/__tests__/use-data.test.js +0 -826
  39. package/src/hooks/use-data.js +0 -143
  40. package/src/util/__tests__/memory-cache.test.js +0 -446
  41. package/src/util/__tests__/request-handler.test.js +0 -121
  42. package/src/util/__tests__/response-cache.test.js +0 -879
  43. package/src/util/memory-cache.js +0 -187
  44. package/src/util/request-handler.js +0 -42
  45. package/src/util/request-handler.md +0 -51
  46. package/src/util/response-cache.js +0 -213
@@ -9,7 +9,6 @@ import type {
9
9
  GqlContext,
10
10
  GqlOperation,
11
11
  GqlFetchOptions,
12
- GqlOperationType,
13
12
  } from "../util/gql-types.js";
14
13
 
15
14
  /**
@@ -17,14 +16,13 @@ import type {
17
16
  *
18
17
  * The fetch function will resolve null if the request was aborted, otherwise
19
18
  * it will resolve the data returned by the GraphQL server.
19
+ *
20
+ * Context is merged with the default context provided to the GqlRouter.
21
+ * Values in the partial context given to the returned fetch function will
22
+ * only be included if they have a value other than undefined.
20
23
  */
21
- export const useGql = (): (<
22
- TType: GqlOperationType,
23
- TData,
24
- TVariables: {...},
25
- TContext: GqlContext,
26
- >(
27
- operation: GqlOperation<TType, TData, TVariables>,
24
+ export const useGql = (): (<TData, TVariables: {...}, TContext: GqlContext>(
25
+ operation: GqlOperation<TData, TVariables>,
28
26
  options?: GqlFetchOptions<TVariables, TContext>,
29
27
  ) => Promise<?TData>) => {
30
28
  // This hook only works if the `GqlRouter` has been used to setup context.
@@ -41,35 +39,45 @@ export const useGql = (): (<
41
39
  // in hooks deps without fear of it triggering extra renders.
42
40
  const gqlFetch = useMemo(
43
41
  () =>
44
- <
45
- TType: GqlOperationType,
46
- TData,
47
- TVariables: {...},
48
- TContext: GqlContext,
49
- >(
50
- operation: GqlOperation<TType, TData, TVariables>,
42
+ <TData, TVariables: {...}, TContext: GqlContext>(
43
+ operation: GqlOperation<TData, TVariables>,
51
44
  options: GqlFetchOptions<TVariables, TContext> = Object.freeze(
52
45
  {},
53
46
  ),
54
47
  ) => {
55
- const {variables, context} = options;
48
+ const {variables, context = {}} = options;
49
+
50
+ // Let's merge the partial context of the fetch with the
51
+ // default context. We deliberately don't spread because
52
+ // spreading would overwrite default context values with
53
+ // undefined if the partial context includes a value explicitly
54
+ // set to undefined. Instead, we use a map/reduce of keys.
55
+ const mergedContext = Object.keys(context).reduce(
56
+ (acc, key) => {
57
+ if (context[key] !== undefined) {
58
+ acc[key] = context[key];
59
+ }
60
+ return acc;
61
+ },
62
+ {...defaultContext},
63
+ );
56
64
 
57
65
  // Invoke the fetch and extract the data.
58
- return fetch(operation, variables, {
59
- ...defaultContext,
60
- ...context,
61
- }).then(getGqlDataFromResponse, (error) => {
62
- // Return null if the request was aborted.
63
- // The only way to detect this reliably, it seems, is to
64
- // check the error name and see if it's "AbortError" (this
65
- // is also what Apollo does).
66
- // Even then, it's reliant on the fetch supporting aborts.
67
- if (error.name === "AbortError") {
68
- return null;
69
- }
70
- // Need to make sure we pass other errors along.
71
- throw error;
72
- });
66
+ return fetch(operation, variables, mergedContext).then(
67
+ getGqlDataFromResponse,
68
+ (error) => {
69
+ // Return null if the request was aborted.
70
+ // The only way to detect this reliably, it seems, is to
71
+ // check the error name and see if it's "AbortError" (this
72
+ // is also what Apollo does).
73
+ // Even then, it's reliant on the fetch supporting aborts.
74
+ if (error.name === "AbortError") {
75
+ return null;
76
+ }
77
+ // Need to make sure we pass other errors along.
78
+ throw error;
79
+ },
80
+ );
73
81
  },
74
82
  [fetch, defaultContext],
75
83
  );
@@ -0,0 +1,45 @@
1
+ // @flow
2
+ import {Server} from "@khanacademy/wonder-blocks-core";
3
+ import {useContext} from "react";
4
+ import {TrackerContext} from "../util/request-tracking.js";
5
+ import {SsrCache} from "../util/ssr-cache.js";
6
+
7
+ import type {CachedResponse, ValidCacheData} from "../util/types.js";
8
+
9
+ /**
10
+ * Hook to perform an asynchronous action during server-side rendering.
11
+ *
12
+ * This hook registers an asynchronous action to be performed during
13
+ * server-side rendering. The action is performed only once, and the result
14
+ * is cached against the given identifier so that subsequent calls return that
15
+ * cached result allowing components to render more of the component.
16
+ *
17
+ * This hook requires the Wonder Blocks Data functionality for resolving
18
+ * pending requests, as well as support for the hydration cache to be
19
+ * embedded into a page so that the result can by hydrated (if that is a
20
+ * requirement).
21
+ *
22
+ * The asynchronous action is never invoked on the client-side.
23
+ */
24
+ export const useServerEffect = <TData: ValidCacheData>(
25
+ id: string,
26
+ handler: () => Promise<?TData>,
27
+ hydrate: boolean = true,
28
+ ): ?CachedResponse<TData> => {
29
+ // If we're server-side or hydrating, we'll have a cached entry to use.
30
+ // So we get that and use it to initialize our state.
31
+ // This works in both hydration and SSR because the very first call to
32
+ // this will have cached data in those cases as it will be present on the
33
+ // initial render - and subsequent renders on the client it will be null.
34
+ const cachedResult = SsrCache.Default.getEntry<TData>(id);
35
+
36
+ // We only track data requests when we are server-side and we don't
37
+ // already have a result, as given by the cachedData (which is also the
38
+ // initial value for the result state).
39
+ const maybeTrack = useContext(TrackerContext);
40
+ if (cachedResult == null && Server.isServerSide()) {
41
+ maybeTrack?.(id, handler, hydrate);
42
+ }
43
+
44
+ return cachedResult;
45
+ };
@@ -0,0 +1,106 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {KindError, Errors} from "@khanacademy/wonder-stuff-core";
4
+ import {ScopedInMemoryCache} from "../util/scoped-in-memory-cache.js";
5
+ import type {ValidCacheData} from "../util/types.js";
6
+
7
+ /**
8
+ * A function for inserting a value into the cache or clearing it.
9
+ */
10
+ type CacheValueFn<TValue: ValidCacheData> = (value: ?TValue) => void;
11
+
12
+ /**
13
+ * This is the cache.
14
+ * It's incredibly complex.
15
+ * Very in-memory. So cache. Such complex. Wow.
16
+ */
17
+ const cache = new ScopedInMemoryCache();
18
+
19
+ /**
20
+ * Clear the in-memory cache or a single scope within it.
21
+ */
22
+ export const clearSharedCache = (scope: string = "") => {
23
+ // If we have a valid scope (empty string is falsy), then clear that scope.
24
+ if (scope && typeof scope === "string") {
25
+ cache.purgeScope(scope);
26
+ } else {
27
+ // Just reset the object. This should be sufficient.
28
+ cache.purgeAll();
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Hook to retrieve data from and store data in an in-memory cache.
34
+ *
35
+ * @returns {[?ReadOnlyCacheValue, CacheValueFn]}
36
+ * Returns an array containing the current cache entry (or undefined), a
37
+ * function to set the cache entry (passing null or undefined to this function
38
+ * will delete the entry).
39
+ *
40
+ * To clear a single scope within the cache or the entire cache,
41
+ * the `clearScopedCache` export is available.
42
+ *
43
+ * NOTE: Unlike useState or useReducer, we don't automatically update folks
44
+ * if the value they reference changes. We might add it later (if we need to),
45
+ * but the likelihood here is that things won't be changing in this cache in a
46
+ * way where we would need that. If we do (and likely only in specific
47
+ * circumstances), we should consider adding a simple boolean useState that can
48
+ * be toggled to cause a rerender whenever the referenced cached data changes
49
+ * so that callers can re-render on cache changes. However, we should make
50
+ * sure this toggling is optional - or we could use a callback argument, to
51
+ * achieve this on an as-needed basis.
52
+ */
53
+ export const useSharedCache = <TValue: ValidCacheData>(
54
+ id: string,
55
+ scope: string,
56
+ initialValue?: ?TValue | (() => ?TValue),
57
+ ): [?TValue, CacheValueFn<TValue>] => {
58
+ // Verify arguments.
59
+ if (!id || typeof id !== "string") {
60
+ throw new KindError(
61
+ "id must be a non-empty string",
62
+ Errors.InvalidInput,
63
+ );
64
+ }
65
+
66
+ if (!scope || typeof scope !== "string") {
67
+ throw new KindError(
68
+ "scope must be a non-empty string",
69
+ Errors.InvalidInput,
70
+ );
71
+ }
72
+
73
+ // Memoize our APIs.
74
+ // This one allows callers to set or replace the cached value.
75
+ const cacheValue = React.useMemo(
76
+ () => (value: ?TValue) =>
77
+ value == null
78
+ ? cache.purge(scope, id)
79
+ : cache.set(scope, id, value),
80
+ [id, scope],
81
+ );
82
+
83
+ // We don't memo-ize the current value, just in case the cache was updated
84
+ // since our last run through. Also, our cache does not know what type it
85
+ // stores, so we have to cast it to the type we're exporting. This is a
86
+ // dev time courtesy, rather than a runtime thing.
87
+ // $FlowIgnore[incompatible-type]
88
+ let currentValue: ?TValue = cache.get(scope, id);
89
+
90
+ // If we have an initial value, we need to add it to the cache
91
+ // and use it as our current value.
92
+ if (currentValue == null && initialValue !== undefined) {
93
+ // Get the initial value.
94
+ const value =
95
+ typeof initialValue === "function" ? initialValue() : initialValue;
96
+
97
+ // Update the cache.
98
+ cacheValue(value);
99
+
100
+ // Make sure we return this value as our current value.
101
+ currentValue = value;
102
+ }
103
+
104
+ // Now we have everything, let's return it.
105
+ return [currentValue, cacheValue];
106
+ };
package/src/index.js CHANGED
@@ -1,25 +1,23 @@
1
1
  // @flow
2
2
  import {Server} from "@khanacademy/wonder-blocks-core";
3
- import {ResponseCache as ResCache} from "./util/response-cache.js";
3
+ import {SsrCache} from "./util/ssr-cache.js";
4
4
  import {RequestTracker} from "./util/request-tracking.js";
5
5
 
6
6
  import type {
7
- ValidData,
8
- CacheEntry,
9
- IRequestHandler,
7
+ ValidCacheData,
8
+ CachedResponse,
10
9
  ResponseCache,
11
10
  } from "./util/types.js";
12
11
 
13
12
  export type {
14
- Cache,
15
- CacheEntry,
16
- Result,
17
- IRequestHandler,
18
13
  ResponseCache,
14
+ CachedResponse,
15
+ Result,
16
+ ScopedCache,
19
17
  } from "./util/types.js";
20
18
 
21
19
  export const initializeCache = (source: ResponseCache): void =>
22
- ResCache.Default.initialize(source);
20
+ SsrCache.Default.initialize(source);
23
21
 
24
22
  export const fulfillAllDataRequests = (): Promise<ResponseCache> => {
25
23
  if (!Server.isServerSide()) {
@@ -37,24 +35,22 @@ export const hasUnfulfilledRequests = (): boolean => {
37
35
  return RequestTracker.Default.hasUnfulfilledRequests;
38
36
  };
39
37
 
40
- export const removeFromCache = <TOptions, TData: ValidData>(
41
- handler: IRequestHandler<TOptions, TData>,
42
- options: TOptions,
43
- ): boolean => ResCache.Default.remove<TOptions, TData>(handler, options);
38
+ export const removeFromCache = (id: string): boolean =>
39
+ SsrCache.Default.remove(id);
44
40
 
45
- export const removeAllFromCache = <TOptions, TData: ValidData>(
46
- handler: IRequestHandler<TOptions, TData>,
41
+ export const removeAllFromCache = (
47
42
  predicate?: (
48
43
  key: string,
49
- cacheEntry: ?$ReadOnly<CacheEntry<TData>>,
44
+ cacheEntry: ?$ReadOnly<CachedResponse<ValidCacheData>>,
50
45
  ) => boolean,
51
- ): number => ResCache.Default.removeAll<TOptions, TData>(handler, predicate);
46
+ ): void => SsrCache.Default.removeAll(predicate);
52
47
 
53
- export {default as RequestHandler} from "./util/request-handler.js";
54
48
  export {default as TrackData} from "./components/track-data.js";
55
49
  export {default as Data} from "./components/data.js";
56
50
  export {default as InterceptData} from "./components/intercept-data.js";
57
- export {useData} from "./hooks/use-data.js";
51
+ export {useServerEffect} from "./hooks/use-server-effect.js";
52
+ export {useSharedCache, clearSharedCache} from "./hooks/use-shared-cache.js";
53
+ export {ScopedInMemoryCache} from "./util/scoped-in-memory-cache.js";
58
54
 
59
55
  // GraphQL
60
56
  export {GqlRouter} from "./components/gql-router.js";
@@ -0,0 +1,19 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ScopedInMemoryCache #set should throw if the id is 1`] = `"id must be non-empty string"`;
4
+
5
+ exports[`ScopedInMemoryCache #set should throw if the id is [Function anonymous] 1`] = `"id must be non-empty string"`;
6
+
7
+ exports[`ScopedInMemoryCache #set should throw if the id is 5 1`] = `"id must be non-empty string"`;
8
+
9
+ exports[`ScopedInMemoryCache #set should throw if the id is null 1`] = `"id must be non-empty string"`;
10
+
11
+ exports[`ScopedInMemoryCache #set should throw if the scope is 1`] = `"scope must be non-empty string"`;
12
+
13
+ exports[`ScopedInMemoryCache #set should throw if the scope is [Function anonymous] 1`] = `"scope must be non-empty string"`;
14
+
15
+ exports[`ScopedInMemoryCache #set should throw if the scope is 5 1`] = `"scope must be non-empty string"`;
16
+
17
+ exports[`ScopedInMemoryCache #set should throw if the scope is null 1`] = `"scope must be non-empty string"`;
18
+
19
+ exports[`ScopedInMemoryCache #set should throw if the value is a function 1`] = `"value must be a non-function value"`;
@@ -1,9 +1,7 @@
1
1
  // @flow
2
- import {ResponseCache} from "../response-cache.js";
2
+ import {SsrCache} from "../ssr-cache.js";
3
3
  import {RequestFulfillment} from "../request-fulfillment.js";
4
4
 
5
- import type {IRequestHandler} from "../types.js";
6
-
7
5
  describe("RequestFulfillment", () => {
8
6
  it("should provide static default instance", () => {
9
7
  // Arrange
@@ -19,93 +17,66 @@ describe("RequestFulfillment", () => {
19
17
  describe("#fulfill", () => {
20
18
  it("should attempt to cache errors caused directly by handlers", async () => {
21
19
  // Arrange
22
- const responseCache = new ResponseCache();
20
+ const responseCache = new SsrCache();
23
21
  const requestFulfillment = new RequestFulfillment(responseCache);
24
22
  const error = new Error("OH NO!");
25
- const fakeBadHandler: IRequestHandler<any, any> = {
26
- fulfillRequest: () => {
27
- throw error;
28
- },
29
- getKey: jest.fn().mockReturnValue("MY_KEY"),
30
- type: "MY_TYPE",
31
- hydrate: true,
23
+ const fakeBadHandler = () => {
24
+ throw error;
32
25
  };
33
26
  const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
34
27
 
35
28
  // Act
36
- await requestFulfillment.fulfill(fakeBadHandler, "OPTIONS");
29
+ await requestFulfillment.fulfill("ID", {
30
+ handler: fakeBadHandler,
31
+ });
37
32
 
38
33
  // Assert
39
- expect(cacheErrorSpy).toHaveBeenCalledWith(
40
- fakeBadHandler,
41
- "OPTIONS",
42
- error,
43
- );
34
+ expect(cacheErrorSpy).toHaveBeenCalledWith("ID", error, true);
44
35
  });
45
36
 
46
37
  it("should cache errors occurring in promises", async () => {
47
38
  // Arrange
48
- const responseCache = new ResponseCache();
39
+ const responseCache = new SsrCache();
49
40
  const requestFulfillment = new RequestFulfillment(responseCache);
50
- const fakeBadRequestHandler: IRequestHandler<string, any> = {
51
- fulfillRequest: () =>
52
- new Promise((resolve, reject) => reject("OH NO!")),
53
- getKey: (o) => o,
54
- type: "BAD_REQUEST",
55
- hydrate: true,
56
- };
41
+ const fakeBadRequestHandler = () =>
42
+ new Promise((resolve, reject) => reject("OH NO!"));
57
43
  const cacheErrorSpy = jest.spyOn(responseCache, "cacheError");
58
44
 
59
45
  // Act
60
- await requestFulfillment.fulfill(fakeBadRequestHandler, "OPTIONS");
46
+ await requestFulfillment.fulfill("ID", {
47
+ handler: fakeBadRequestHandler,
48
+ });
61
49
 
62
50
  // Assert
63
- expect(cacheErrorSpy).toHaveBeenCalledWith(
64
- fakeBadRequestHandler,
65
- "OPTIONS",
66
- "OH NO!",
67
- );
51
+ expect(cacheErrorSpy).toHaveBeenCalledWith("ID", "OH NO!", true);
68
52
  });
69
53
 
70
54
  it("should cache data from requests", async () => {
71
55
  // Arrange
72
- const responseCache = new ResponseCache();
56
+ const responseCache = new SsrCache();
73
57
  const requestFulfillment = new RequestFulfillment(responseCache);
74
- const fakeRequestHandler: IRequestHandler<string, any> = {
75
- fulfillRequest: () => Promise.resolve("DATA!"),
76
- getKey: (o) => o,
77
- type: "VALID_REQUEST",
78
- hydrate: true,
79
- };
58
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
80
59
  const cacheDataSpy = jest.spyOn(responseCache, "cacheData");
81
60
 
82
61
  // Act
83
- await requestFulfillment.fulfill(fakeRequestHandler, "OPTIONS");
62
+ await requestFulfillment.fulfill("ID", {
63
+ handler: fakeRequestHandler,
64
+ });
84
65
 
85
66
  // Assert
86
- expect(cacheDataSpy).toHaveBeenCalledWith(
87
- fakeRequestHandler,
88
- "OPTIONS",
89
- "DATA!",
90
- );
67
+ expect(cacheDataSpy).toHaveBeenCalledWith("ID", "DATA!", true);
91
68
  });
92
69
 
93
70
  it("should return a promise of the result", async () => {
94
71
  // Arrange
95
- const responseCache = new ResponseCache();
72
+ const responseCache = new SsrCache();
96
73
  const requestFulfillment = new RequestFulfillment(responseCache);
97
- const fakeRequestHandler: IRequestHandler<string, any> = {
98
- fulfillRequest: () => Promise.resolve("DATA!"),
99
- getKey: (o) => o,
100
- type: "VALID_REQUEST",
101
- hydrate: true,
102
- };
74
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
103
75
 
104
76
  // Act
105
- const result = await requestFulfillment.fulfill(
106
- fakeRequestHandler,
107
- "OPTIONS",
108
- );
77
+ const result = await requestFulfillment.fulfill("ID", {
78
+ handler: fakeRequestHandler,
79
+ });
109
80
 
110
81
  // Assert
111
82
  expect(result).toStrictEqual({
@@ -115,24 +86,17 @@ describe("RequestFulfillment", () => {
115
86
 
116
87
  it("should reuse inflight requests", () => {
117
88
  // Arrange
118
- const responseCache = new ResponseCache();
89
+ const responseCache = new SsrCache();
119
90
  const requestFulfillment = new RequestFulfillment(responseCache);
120
- const fakeRequestHandler: IRequestHandler<string, any> = {
121
- fulfillRequest: () => Promise.resolve("DATA!"),
122
- getKey: (o) => o,
123
- type: "VALID_REQUEST",
124
- hydrate: true,
125
- };
91
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
126
92
 
127
93
  // Act
128
- const promise = requestFulfillment.fulfill(
129
- fakeRequestHandler,
130
- "OPTIONS",
131
- );
132
- const result = requestFulfillment.fulfill(
133
- fakeRequestHandler,
134
- "OPTIONS",
135
- );
94
+ const promise = requestFulfillment.fulfill("ID", {
95
+ handler: fakeRequestHandler,
96
+ });
97
+ const result = requestFulfillment.fulfill("ID", {
98
+ handler: fakeRequestHandler,
99
+ });
136
100
 
137
101
  // Assert
138
102
  expect(result).toBe(promise);
@@ -140,25 +104,18 @@ describe("RequestFulfillment", () => {
140
104
 
141
105
  it("should remove inflight requests upon completion", async () => {
142
106
  // Arrange
143
- const responseCache = new ResponseCache();
107
+ const responseCache = new SsrCache();
144
108
  const requestFulfillment = new RequestFulfillment(responseCache);
145
- const fakeRequestHandler: IRequestHandler<string, any> = {
146
- fulfillRequest: () => Promise.resolve("DATA!"),
147
- getKey: (o) => o,
148
- type: "VALID_REQUEST",
149
- hydrate: false,
150
- };
109
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
151
110
 
152
111
  // Act
153
- const promise = requestFulfillment.fulfill(
154
- fakeRequestHandler,
155
- "OPTIONS",
156
- );
112
+ const promise = requestFulfillment.fulfill("ID", {
113
+ handler: fakeRequestHandler,
114
+ });
157
115
  await promise;
158
- const result = requestFulfillment.fulfill(
159
- fakeRequestHandler,
160
- "OPTIONS",
161
- );
116
+ const result = requestFulfillment.fulfill("ID", {
117
+ handler: fakeRequestHandler,
118
+ });
162
119
 
163
120
  // Assert
164
121
  expect(result).not.toBe(promise);