@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
@@ -2,18 +2,18 @@ When you want to generate tests that check the loading state and
2
2
  subsequent loaded state are working correctly for your uses of `Data` you can
3
3
  use the `InterceptData` component.
4
4
 
5
- This component takes four props; children to be rendered, the handler of the
6
- type of data requests that are to be intercepted, and a `fulfillRequest`.
5
+ This component takes three props; children to be rendered, the handler of the
6
+ to fulfill the request, and the id of the request that is being intercepted.
7
7
 
8
8
  Note that this component is expected to be used only within test cases and
9
9
  usually only as a single instance. In flight requests for a given handler
10
10
  type can be shared and as such, using `InterceptData` alongside non-intercepted
11
- `Data` components with the same handler type can have indeterminate outcomes.
11
+ `Data` components with the same id can have indeterminate outcomes.
12
12
 
13
- The `fulfillRequest` intercept function has the form:
13
+ The `handler` intercept function has the form:
14
14
 
15
15
  ```js static
16
- (options: TOptions) => ?Promise<TData>;
16
+ () => ?Promise<?TData>;
17
17
  ```
18
18
 
19
19
  If this method returns `null`, the default behavior occurs. This
@@ -23,40 +23,26 @@ means that a request will be made for data via the handler assigned to the
23
23
  ```jsx
24
24
  import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
25
25
  import {View} from "@khanacademy/wonder-blocks-core";
26
- import {InterceptData, Data, RequestHandler} from "@khanacademy/wonder-blocks-data";
26
+ import {InterceptData, Data} from "@khanacademy/wonder-blocks-data";
27
27
  import {Strut} from "@khanacademy/wonder-blocks-layout";
28
28
  import Color from "@khanacademy/wonder-blocks-color";
29
29
  import Spacing from "@khanacademy/wonder-blocks-spacing";
30
30
 
31
- class MyHandler extends RequestHandler {
32
- constructor() {
33
- super("INTERCEPT_DATA_HANDLER1");
34
- }
31
+ const myHandler = () => Promise.reject(new Error("You should not see this!"));
35
32
 
36
- fulfillRequest(options) {
37
- return Promise.reject(new Error("You should not see this!"));
38
- }
39
- }
33
+ const interceptHandler = () => Promise.resolve("INTERCEPTED DATA!");
40
34
 
41
- const handler = new MyHandler();
42
- const fulfillRequestInterceptor = function(options) {
43
- if (options === "DATA") {
44
- return Promise.resolve("INTERCEPTED DATA!");
45
- }
46
- return null;
47
- };
48
-
49
- <InterceptData handler={handler} fulfillRequest={fulfillRequestInterceptor}>
35
+ <InterceptData handler={interceptHandler} requestId="INTERCEPT_EXAMPLE">
50
36
  <View>
51
37
  <Body>This received intercepted data!</Body>
52
- <Data handler={handler} options={"DATA"}>
53
- {({loading, data}) => {
54
- if (loading) {
38
+ <Data handler={myHandler} requestId="INTERCEPT_EXAMPLE">
39
+ {(result) => {
40
+ if (result.status !== "success") {
55
41
  return "If you see this, the example is broken!";
56
42
  }
57
43
 
58
44
  return (
59
- <BodyMonospace>{data}</BodyMonospace>
45
+ <BodyMonospace>{result.data}</BodyMonospace>
60
46
  );
61
47
  }}
62
48
  </Data>
@@ -65,19 +65,11 @@ import {Strut} from "@khanacademy/wonder-blocks-layout";
65
65
  import Spacing from "@khanacademy/wonder-blocks-spacing";
66
66
  import Button from "@khanacademy/wonder-blocks-button";
67
67
  import {Server, View} from "@khanacademy/wonder-blocks-core";
68
- import {Data, TrackData, RequestHandler, fulfillAllDataRequests} from "@khanacademy/wonder-blocks-data";
68
+ import {Data, TrackData, fulfillAllDataRequests} from "@khanacademy/wonder-blocks-data";
69
69
 
70
- class MyPretendHandler extends RequestHandler {
71
- constructor() {
72
- super("MY_PRETEND_HANDLER");
73
- }
74
-
75
- fulfillRequest(options) {
76
- return new Promise((resolve, reject) =>
77
- setTimeout(() => resolve("DATA!"), 3000),
78
- );
79
- }
80
- }
70
+ const myPretendHandler = () => new Promise((resolve, reject) =>
71
+ setTimeout(() => resolve("DATA!"), 3000),
72
+ );
81
73
 
82
74
  class Example extends React.Component {
83
75
  constructor() {
@@ -87,7 +79,6 @@ class Example extends React.Component {
87
79
  * for the scope of this component.
88
80
  */
89
81
  this.state = {};
90
- this._handler = new MyPretendHandler();
91
82
  }
92
83
 
93
84
  static getDerivedStateFromError(error) {
@@ -133,15 +124,16 @@ class Example extends React.Component {
133
124
  const data = this.state.data
134
125
  ? JSON.stringify(this.state.data, undefined, " ")
135
126
  : "Data requested...";
127
+
136
128
  return (
137
129
  <React.Fragment>
138
130
  <Strut size={Spacing.small_12} />
139
131
  <TrackData>
140
- <Data handler={this._handler} options={{}}>
141
- {({loading, data, error}) => (
132
+ <Data handler={myPretendHandler} requestId="TRACK_DATA_EXAMPLE">
133
+ {(result) => (
142
134
  <View>
143
- <BodyMonospace>{`Loading: ${loading}`}</BodyMonospace>
144
- <BodyMonospace>{`Data: ${JSON.stringify(data)}`}</BodyMonospace>
135
+ <BodyMonospace>{`Loading: ${result.status === "loading"}`}</BodyMonospace>
136
+ <BodyMonospace>{`Data: ${JSON.stringify(result.data)}`}</BodyMonospace>
145
137
  </View>
146
138
  )}
147
139
  </Data>
@@ -162,12 +154,6 @@ class Example extends React.Component {
162
154
  rendered tree.
163
155
  </Body>
164
156
  <Strut size={Spacing.small_12} />
165
- <Body>
166
- If you click to remount after the data appears, we'll
167
- rerender with the now cached data, and above should
168
- update accordingly.
169
- </Body>
170
- <Strut size={Spacing.small_12} />
171
157
  <BodyMonospace>{data}</BodyMonospace>
172
158
  </View>
173
159
  </React.Fragment>
@@ -0,0 +1,17 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`#useSharedCache should throw if the id is 1`] = `[InvalidInputError: id must be a non-empty string]`;
4
+
5
+ exports[`#useSharedCache should throw if the id is [Function anonymous] 1`] = `[InvalidInputError: id must be a non-empty string]`;
6
+
7
+ exports[`#useSharedCache should throw if the id is 5 1`] = `[InvalidInputError: id must be a non-empty string]`;
8
+
9
+ exports[`#useSharedCache should throw if the id is null 1`] = `[InvalidInputError: id must be a non-empty string]`;
10
+
11
+ exports[`#useSharedCache should throw if the scope is 1`] = `[InvalidInputError: scope must be a non-empty string]`;
12
+
13
+ exports[`#useSharedCache should throw if the scope is [Function anonymous] 1`] = `[InvalidInputError: scope must be a non-empty string]`;
14
+
15
+ exports[`#useSharedCache should throw if the scope is 5 1`] = `[InvalidInputError: scope must be a non-empty string]`;
16
+
17
+ exports[`#useSharedCache should throw if the scope is null 1`] = `[InvalidInputError: scope must be a non-empty string]`;
@@ -80,6 +80,7 @@ describe("#useGql", () => {
80
80
  id: "MyQuery",
81
81
  };
82
82
  const gqlOpContext = {
83
+ a: undefined, // This should not get included.
83
84
  b: "overrideB",
84
85
  };
85
86
  const gqlOpVariables = {
@@ -0,0 +1,217 @@
1
+ // @flow
2
+ import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
3
+ import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
4
+
5
+ import {Server} from "@khanacademy/wonder-blocks-core";
6
+
7
+ import TrackData from "../../components/track-data.js";
8
+ import {RequestFulfillment} from "../../util/request-fulfillment.js";
9
+ import {SsrCache} from "../../util/ssr-cache.js";
10
+ import {RequestTracker} from "../../util/request-tracking.js";
11
+
12
+ import {useServerEffect} from "../use-server-effect.js";
13
+
14
+ describe("#useServerEffect", () => {
15
+ beforeEach(() => {
16
+ const responseCache = new SsrCache();
17
+ jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
18
+ jest.spyOn(RequestFulfillment, "Default", "get").mockReturnValue(
19
+ new RequestFulfillment(responseCache),
20
+ );
21
+ jest.spyOn(RequestTracker, "Default", "get").mockReturnValue(
22
+ new RequestTracker(responseCache),
23
+ );
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.resetAllMocks();
28
+ });
29
+
30
+ describe("when server-side", () => {
31
+ beforeEach(() => {
32
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
33
+ });
34
+
35
+ it("should return null if no cached result", () => {
36
+ // Arrange
37
+ const fakeHandler = jest.fn();
38
+
39
+ // Act
40
+ const {
41
+ result: {current: result},
42
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
43
+
44
+ // Assert
45
+ expect(result).toBeNull();
46
+ });
47
+
48
+ it("should not directly request fulfillment", () => {
49
+ // Arrange
50
+ const fakeHandler = jest.fn();
51
+ const fulfillRequestSpy = jest.spyOn(
52
+ RequestFulfillment.Default,
53
+ "fulfill",
54
+ );
55
+
56
+ // Act
57
+ serverRenderHook(() => useServerEffect("ID", fakeHandler));
58
+
59
+ // Assert
60
+ expect(fulfillRequestSpy).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("should track the request", () => {
64
+ // Arrange
65
+ const fakeHandler = jest.fn();
66
+ const trackDataRequestSpy = jest.spyOn(
67
+ RequestTracker.Default,
68
+ "trackDataRequest",
69
+ );
70
+
71
+ // Act
72
+ serverRenderHook(() => useServerEffect("ID", fakeHandler), {
73
+ wrapper: TrackData,
74
+ });
75
+
76
+ // Assert
77
+ expect(trackDataRequestSpy).toHaveBeenCalledWith(
78
+ "ID",
79
+ fakeHandler,
80
+ true,
81
+ );
82
+ });
83
+
84
+ it("should return data cached result", () => {
85
+ // Arrange
86
+ const fakeHandler = jest.fn();
87
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
88
+ data: "DATA",
89
+ error: null,
90
+ });
91
+
92
+ // Act
93
+ const {
94
+ result: {current: result},
95
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
96
+
97
+ // Assert
98
+ expect(result).toEqual({data: "DATA", error: null});
99
+ });
100
+
101
+ it("should return error cached result", () => {
102
+ // Arrange
103
+ const fakeHandler = jest.fn();
104
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
105
+ data: null,
106
+ error: "ERROR",
107
+ });
108
+
109
+ // Act
110
+ const {
111
+ result: {current: result},
112
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
113
+
114
+ // Assert
115
+ expect(result).toEqual({
116
+ data: null,
117
+ error: "ERROR",
118
+ });
119
+ });
120
+ });
121
+
122
+ describe("when client-side", () => {
123
+ beforeEach(() => {
124
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
125
+ });
126
+
127
+ it("should return null if no cached result", () => {
128
+ // Arrange
129
+ const fakeHandler = jest.fn();
130
+
131
+ // Act
132
+ const {
133
+ result: {current: result},
134
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
135
+
136
+ // Assert
137
+ expect(result).toBeNull();
138
+ });
139
+
140
+ it("should return data cached result", () => {
141
+ // Arrange
142
+ const fakeHandler = jest.fn();
143
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
144
+ data: "DATA",
145
+ error: null,
146
+ });
147
+
148
+ // Act
149
+ const {
150
+ result: {current: result},
151
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
152
+
153
+ // Assert
154
+ expect(result).toEqual({data: "DATA", error: null});
155
+ });
156
+
157
+ it("should return error cached result", () => {
158
+ // Arrange
159
+ const fakeHandler = jest.fn();
160
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
161
+ data: null,
162
+ error: "ERROR",
163
+ });
164
+
165
+ // Act
166
+ const {
167
+ result: {current: result},
168
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
169
+
170
+ // Assert
171
+ expect(result).toEqual({
172
+ data: null,
173
+ error: "ERROR",
174
+ });
175
+ });
176
+
177
+ it("should not track the request", () => {
178
+ // Arrange
179
+ const fakeHandler = jest.fn().mockReturnValue(
180
+ new Promise(() => {
181
+ /*prevent act() warning*/
182
+ }),
183
+ );
184
+ const trackDataRequestSpy = jest.spyOn(
185
+ RequestTracker.Default,
186
+ "trackDataRequest",
187
+ );
188
+
189
+ // Act
190
+ clientRenderHook(() => useServerEffect("ID", fakeHandler), {
191
+ wrapper: TrackData,
192
+ });
193
+
194
+ // Assert
195
+ expect(trackDataRequestSpy).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("should not request fulfillment", () => {
199
+ // Arrange
200
+ const fakeHandler = jest.fn().mockReturnValue(
201
+ new Promise(() => {
202
+ /*prevent act() warning*/
203
+ }),
204
+ );
205
+ const fulfillRequestSpy = jest.spyOn(
206
+ RequestFulfillment.Default,
207
+ "fulfill",
208
+ );
209
+
210
+ // Act
211
+ clientRenderHook(() => useServerEffect("ID", fakeHandler));
212
+
213
+ // Assert
214
+ expect(fulfillRequestSpy).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,307 @@
1
+ // @flow
2
+ import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
3
+
4
+ import {useSharedCache, clearSharedCache} from "../use-shared-cache.js";
5
+
6
+ describe("#useSharedCache", () => {
7
+ beforeEach(() => {
8
+ clearSharedCache();
9
+ });
10
+
11
+ it.each`
12
+ id
13
+ ${null}
14
+ ${""}
15
+ ${5}
16
+ ${() => "BOO"}
17
+ `("should throw if the id is $id", ({id}) => {
18
+ // Arrange
19
+
20
+ // Act
21
+ const {result} = clientRenderHook(() => useSharedCache(id, "scope"));
22
+
23
+ // Assert
24
+ expect(result.error).toMatchSnapshot();
25
+ });
26
+
27
+ it.each`
28
+ scope
29
+ ${null}
30
+ ${""}
31
+ ${5}
32
+ ${() => "BOO"}
33
+ `("should throw if the scope is $scope", ({scope}) => {
34
+ // Arrange
35
+
36
+ // Act
37
+ const {result} = clientRenderHook(() => useSharedCache("id", scope));
38
+
39
+ // Assert
40
+ expect(result.error).toMatchSnapshot();
41
+ });
42
+
43
+ it("should return a tuple of two items", () => {
44
+ // Arrange
45
+
46
+ // Act
47
+ const {
48
+ result: {current: result},
49
+ } = clientRenderHook(() => useSharedCache("id", "scope"));
50
+
51
+ // Assert
52
+ expect(result).toBeArrayOfSize(2);
53
+ });
54
+
55
+ describe("tuple[0] - currentValue", () => {
56
+ it("should be null if nothing is cached", () => {
57
+ // Arrange
58
+
59
+ // Act
60
+ const {
61
+ result: {current: result},
62
+ } = clientRenderHook(() => useSharedCache("id", "scope"));
63
+
64
+ // Assert
65
+ expect(result[0]).toBeNull();
66
+ });
67
+
68
+ it("should match initialValue when provided as a non-function", () => {
69
+ // Arrange
70
+
71
+ // Act
72
+ const {
73
+ result: {current: result},
74
+ } = clientRenderHook(() =>
75
+ useSharedCache("id", "scope", "INITIAL VALUE"),
76
+ );
77
+
78
+ // Assert
79
+ expect(result[0]).toBe("INITIAL VALUE");
80
+ });
81
+
82
+ it("should match the return of initialValue when provided as non-function", () => {
83
+ // Arrange
84
+
85
+ // Act
86
+ const {
87
+ result: {current: result},
88
+ } = clientRenderHook(() =>
89
+ useSharedCache("id", "scope", () => "INITIAL VALUE"),
90
+ );
91
+
92
+ // Assert
93
+ expect(result[0]).toBe("INITIAL VALUE");
94
+ });
95
+ });
96
+
97
+ describe("tuple[1] - setValue", () => {
98
+ it("should be a function", () => {
99
+ // Arrange
100
+
101
+ // Act
102
+ const {
103
+ result: {current: result},
104
+ } = clientRenderHook(() => useSharedCache("id", "scope"));
105
+
106
+ // Assert
107
+ expect(result[1]).toBeFunction();
108
+ });
109
+
110
+ it("should be the same function if the id and scope remain the same", () => {
111
+ // Arrange
112
+ const wrapper = clientRenderHook(
113
+ ({id, scope}) => useSharedCache(id, scope),
114
+ {initialProps: {id: "id", scope: "scope"}},
115
+ );
116
+
117
+ // Act
118
+ wrapper.rerender({
119
+ id: "id",
120
+ scope: "scope",
121
+ });
122
+ const result1 = wrapper.result.all[wrapper.result.all.length - 2];
123
+ const result2 = wrapper.result.current;
124
+
125
+ // Assert
126
+ // $FlowIgnore[prop-missing]
127
+ expect(result1[1]).toBe(result2[1]);
128
+ });
129
+
130
+ it("should be a new function if the id changes", () => {
131
+ // Arrange
132
+ const wrapper = clientRenderHook(
133
+ ({id}) => useSharedCache(id, "scope"),
134
+ {
135
+ initialProps: {id: "id"},
136
+ },
137
+ );
138
+
139
+ // Act
140
+ wrapper.rerender({id: "new-id"});
141
+ const result1 = wrapper.result.all[wrapper.result.all.length - 2];
142
+ const result2 = wrapper.result.current;
143
+
144
+ // Assert
145
+ // $FlowIgnore[prop-missing]
146
+ expect(result1[1]).not.toBe(result2[1]);
147
+ });
148
+
149
+ it("should be a new function if the scope changes", () => {
150
+ // Arrange
151
+ const wrapper = clientRenderHook(
152
+ ({scope}) => useSharedCache("id", scope),
153
+ {
154
+ initialProps: {scope: "scope"},
155
+ },
156
+ );
157
+
158
+ // Act
159
+ wrapper.rerender({scope: "new-scope"});
160
+ const result1 = wrapper.result.all[wrapper.result.all.length - 2];
161
+ const result2 = wrapper.result.current;
162
+
163
+ // Assert
164
+ // $FlowIgnore[prop-missing]
165
+ expect(result1[1]).not.toBe(result2[1]);
166
+ });
167
+
168
+ it("should set the value in the cache", () => {
169
+ // Arrange
170
+ const wrapper = clientRenderHook(() =>
171
+ useSharedCache("id", "scope"),
172
+ );
173
+ const setValue = wrapper.result.current[1];
174
+
175
+ // Act
176
+ setValue("CACHED_VALUE");
177
+ // Rerender so the hook retrieves this new value.
178
+ wrapper.rerender();
179
+ const result = wrapper.result.current[0];
180
+
181
+ // Assert
182
+ expect(result).toBe("CACHED_VALUE");
183
+ });
184
+
185
+ it.each`
186
+ value
187
+ ${undefined}
188
+ ${null}
189
+ `("should purge the value from the cache if $value", ({value}) => {
190
+ // Arrange
191
+ const wrapper = clientRenderHook(() =>
192
+ useSharedCache("id", "scope"),
193
+ );
194
+ const setValue = wrapper.result.current[1];
195
+ setValue("CACHED_VALUE");
196
+
197
+ // Act
198
+ // Rerender so the result has the cached value.
199
+ wrapper.rerender();
200
+ setValue(value);
201
+ // Rerender so the hook retrieves this new value.
202
+ wrapper.rerender();
203
+ const result = wrapper.result.current[0];
204
+
205
+ // Assert
206
+ expect(result).toBeNull();
207
+ });
208
+ });
209
+
210
+ it("should share cache across all uses", () => {
211
+ // Arrange
212
+ const hook1 = clientRenderHook(() => useSharedCache("id", "scope"));
213
+ const hook2 = clientRenderHook(() => useSharedCache("id", "scope"));
214
+ hook1.result.current[1]("VALUE_1");
215
+
216
+ // Act
217
+ hook2.rerender();
218
+ const result = hook2.result.current[0];
219
+
220
+ // Assert
221
+ expect(result).toBe("VALUE_1");
222
+ });
223
+
224
+ it.each`
225
+ id
226
+ ${"id1"}
227
+ ${"id2"}
228
+ `("should not share cache if scope is different", ({id}) => {
229
+ // Arrange
230
+ const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
231
+ const hook2 = clientRenderHook(() => useSharedCache(id, "scope2"));
232
+ hook1.result.current[1]("VALUE_1");
233
+
234
+ // Act
235
+ hook2.rerender();
236
+ const result = hook2.result.current[0];
237
+
238
+ // Assert
239
+ expect(result).toBeNull();
240
+ });
241
+
242
+ it.each`
243
+ scope
244
+ ${"scope1"}
245
+ ${"scope2"}
246
+ `("should not share cache if id is different", ({scope}) => {
247
+ // Arrange
248
+ const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
249
+ const hook2 = clientRenderHook(() => useSharedCache("id2", scope));
250
+ hook1.result.current[1]("VALUE_1");
251
+
252
+ // Act
253
+ hook2.rerender();
254
+ const result = hook2.result.current[0];
255
+
256
+ // Assert
257
+ expect(result).toBeNull();
258
+ });
259
+ });
260
+
261
+ describe("#clearSharedCache", () => {
262
+ beforeEach(() => {
263
+ clearSharedCache();
264
+ });
265
+
266
+ it("should clear the entire cache if no scope given", () => {
267
+ // Arrange
268
+ const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
269
+ const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
270
+ hook1.result.current[1]("VALUE_1");
271
+ hook2.result.current[1]("VALUE_2");
272
+ // Make sure both hook results include the updated value.
273
+ hook1.rerender();
274
+ hook2.rerender();
275
+
276
+ // Act
277
+ clearSharedCache();
278
+ // Make sure we refresh the hook results.
279
+ hook1.rerender();
280
+ hook2.rerender();
281
+
282
+ // Assert
283
+ expect(hook1.result.current[0]).toBeNull();
284
+ expect(hook2.result.current[0]).toBeNull();
285
+ });
286
+
287
+ it("should clear the given scope only", () => {
288
+ // Arrange
289
+ const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
290
+ const hook2 = clientRenderHook(() => useSharedCache("id2", "scope2"));
291
+ hook1.result.current[1]("VALUE_1");
292
+ hook2.result.current[1]("VALUE_2");
293
+ // Make sure both hook results include the updated value.
294
+ hook1.rerender();
295
+ hook2.rerender();
296
+
297
+ // Act
298
+ clearSharedCache("scope2");
299
+ // Make sure we refresh the hook results.
300
+ hook1.rerender();
301
+ hook2.rerender();
302
+
303
+ // Assert
304
+ expect(hook1.result.current[0]).toBe("VALUE_1");
305
+ expect(hook2.result.current[0]).toBeNull();
306
+ });
307
+ });