@croct/plug-react 0.4.2 → 0.5.0-next.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.
Files changed (92) hide show
  1. package/CroctProvider.d.ts +7 -7
  2. package/CroctProvider.js +37 -0
  3. package/CroctProvider.js.map +1 -0
  4. package/README.md +245 -107
  5. package/api/evaluate.d.ts +7 -0
  6. package/api/evaluate.js +15 -0
  7. package/api/evaluate.js.map +1 -0
  8. package/api/fetchContent.d.ts +13 -0
  9. package/api/fetchContent.js +20 -0
  10. package/api/fetchContent.js.map +1 -0
  11. package/api/index.d.ts +2 -0
  12. package/api/index.js +19 -0
  13. package/api/index.js.map +1 -0
  14. package/components/Personalization/index.d.ts +10 -10
  15. package/components/Personalization/index.js +13 -0
  16. package/components/Personalization/index.js.map +1 -0
  17. package/components/Slot/index.d.ts +19 -19
  18. package/components/Slot/index.js +13 -0
  19. package/components/Slot/index.js.map +1 -0
  20. package/components/index.d.ts +2 -2
  21. package/components/index.js +19 -0
  22. package/components/index.js.map +1 -0
  23. package/hooks/Cache.d.ts +22 -22
  24. package/hooks/Cache.js +62 -0
  25. package/hooks/Cache.js.map +1 -0
  26. package/hooks/index.d.ts +3 -3
  27. package/hooks/index.js +20 -0
  28. package/hooks/index.js.map +1 -0
  29. package/hooks/useContent.d.ts +18 -17
  30. package/hooks/useContent.js +25 -0
  31. package/hooks/useContent.js.map +1 -0
  32. package/hooks/useCroct.d.ts +2 -2
  33. package/hooks/useCroct.js +14 -0
  34. package/hooks/useCroct.js.map +1 -0
  35. package/hooks/useEvaluation.d.ts +11 -11
  36. package/hooks/useEvaluation.js +35 -0
  37. package/hooks/useEvaluation.js.map +1 -0
  38. package/hooks/useLoader.d.ts +5 -5
  39. package/hooks/useLoader.js +41 -0
  40. package/hooks/useLoader.js.map +1 -0
  41. package/index.d.ts +5 -3
  42. package/index.js +20 -337
  43. package/index.js.map +1 -1
  44. package/package.json +33 -46
  45. package/src/api/evaluate.test.ts +59 -0
  46. package/src/api/evaluate.ts +19 -0
  47. package/src/api/fetchContent.test.ts +134 -0
  48. package/src/api/fetchContent.ts +47 -0
  49. package/src/api/index.ts +2 -0
  50. package/src/components/index.ts +2 -0
  51. package/src/global.d.ts +7 -0
  52. package/src/hooks/Cache.test.ts +280 -0
  53. package/src/hooks/Cache.ts +97 -0
  54. package/src/hooks/index.ts +3 -0
  55. package/src/hooks/useContent.ssr.test.ts +23 -0
  56. package/src/hooks/useContent.test.ts +66 -0
  57. package/src/hooks/useContent.ts +69 -0
  58. package/src/hooks/useCroct.ts +13 -0
  59. package/src/hooks/useEvaluation.ssr.test.ts +23 -0
  60. package/src/hooks/useEvaluation.test.ts +92 -0
  61. package/src/hooks/useEvaluation.ts +58 -0
  62. package/src/hooks/useLoader.test.ts +320 -0
  63. package/src/hooks/useLoader.ts +50 -0
  64. package/src/index.ts +5 -0
  65. package/src/react-app-env.d.ts +1 -0
  66. package/src/ssr-polyfills.ssr.test.ts +46 -0
  67. package/src/ssr-polyfills.test.ts +65 -0
  68. package/src/ssr-polyfills.ts +68 -0
  69. package/ssr-polyfills.d.ts +3 -3
  70. package/ssr-polyfills.js +64 -0
  71. package/ssr-polyfills.js.map +1 -0
  72. package/CroctProvider.test.d.ts +0 -1
  73. package/components/Personalization/index.d.test.d.ts +0 -1
  74. package/components/Personalization/index.stories.d.ts +0 -7
  75. package/components/Personalization/index.test.d.ts +0 -1
  76. package/components/Slot/index.d.test.d.ts +0 -1
  77. package/components/Slot/index.stories.d.ts +0 -17
  78. package/components/Slot/index.test.d.ts +0 -1
  79. package/hooks/Cache.test.d.ts +0 -1
  80. package/hooks/useContent.d.test.d.ts +0 -1
  81. package/hooks/useContent.ssr.test.d.ts +0 -1
  82. package/hooks/useContent.stories.d.ts +0 -19
  83. package/hooks/useContent.test.d.ts +0 -1
  84. package/hooks/useCroct.ssr.test.d.ts +0 -1
  85. package/hooks/useCroct.test.d.ts +0 -1
  86. package/hooks/useEvaluation.d.test.d.ts +0 -1
  87. package/hooks/useEvaluation.ssr.test.d.ts +0 -1
  88. package/hooks/useEvaluation.stories.d.ts +0 -8
  89. package/hooks/useEvaluation.test.d.ts +0 -1
  90. package/hooks/useLoader.test.d.ts +0 -1
  91. package/ssr-polyfills.ssr.test.d.ts +0 -1
  92. package/ssr-polyfills.test.d.ts +0 -1
@@ -0,0 +1,134 @@
1
+ import {ContentFetcher} from '@croct/sdk/contentFetcher';
2
+ import {FetchResponse} from '@croct/plug/plug';
3
+ import {fetchContent, FetchOptions} from './fetchContent';
4
+
5
+ const mockFetch: ContentFetcher['fetch'] = jest.fn();
6
+
7
+ jest.mock(
8
+ '@croct/sdk/contentFetcher',
9
+ () => ({
10
+ __esModule: true,
11
+ /*
12
+ * eslint-disable-next-line prefer-arrow-callback --
13
+ * The mock can't be an arrow function because calling new on
14
+ * an arrow function is not allowed in JavaScript.
15
+ */
16
+ ContentFetcher: jest.fn(function constructor(this: ContentFetcher) {
17
+ this.fetch = mockFetch;
18
+ }),
19
+ }),
20
+ );
21
+
22
+ describe('fetchContent', () => {
23
+ const apiKey = '00000000-0000-0000-0000-000000000000';
24
+
25
+ afterEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ it('should forward the call to the content fetcher', async () => {
30
+ const slotId = 'slot-id';
31
+
32
+ const options: FetchOptions = {
33
+ apiKey: apiKey,
34
+ timeout: 100,
35
+ };
36
+
37
+ const result: FetchResponse<typeof slotId> = {
38
+ content: {
39
+ id: 'test',
40
+ },
41
+ };
42
+
43
+ jest.mocked(mockFetch).mockResolvedValue(result);
44
+
45
+ await expect(fetchContent(slotId, options)).resolves.toEqual(result);
46
+
47
+ expect(ContentFetcher).toHaveBeenCalledWith({
48
+ apiKey: options.apiKey,
49
+ });
50
+
51
+ expect(mockFetch).toHaveBeenCalledWith(slotId, {
52
+ timeout: options.timeout,
53
+ });
54
+ });
55
+
56
+ it('should extract the slot ID and version', async () => {
57
+ const slotId = 'slot-id';
58
+ const version = '1';
59
+ const versionedSlotId = `${slotId}@${version}`;
60
+
61
+ const options: FetchOptions = {
62
+ apiKey: apiKey,
63
+ timeout: 100,
64
+ };
65
+
66
+ const result: FetchResponse<typeof slotId> = {
67
+ content: {
68
+ id: 'test',
69
+ },
70
+ };
71
+
72
+ jest.mocked(mockFetch).mockResolvedValue(result);
73
+
74
+ await expect(fetchContent(versionedSlotId, options)).resolves.toEqual(result);
75
+
76
+ expect(ContentFetcher).toHaveBeenCalledWith({
77
+ apiKey: options.apiKey,
78
+ });
79
+
80
+ expect(mockFetch).toHaveBeenCalledWith(slotId, {
81
+ timeout: options.timeout,
82
+ version: version,
83
+ });
84
+ });
85
+
86
+ it('should fetch content omitting the latest alias', async () => {
87
+ const slotId = 'slot-id';
88
+ const version = 'latest';
89
+ const versionedSlotId = `${slotId}@${version}`;
90
+
91
+ const options: FetchOptions = {
92
+ apiKey: apiKey,
93
+ timeout: 100,
94
+ };
95
+
96
+ const result: FetchResponse<typeof slotId> = {
97
+ content: {
98
+ id: 'test',
99
+ },
100
+ };
101
+
102
+ jest.mocked(mockFetch).mockResolvedValue(result);
103
+
104
+ await expect(fetchContent(versionedSlotId, options)).resolves.toEqual(result);
105
+
106
+ expect(ContentFetcher).toHaveBeenCalledWith({
107
+ apiKey: options.apiKey,
108
+ });
109
+
110
+ expect(mockFetch).toHaveBeenCalledWith(slotId, {
111
+ timeout: options.timeout,
112
+ });
113
+ });
114
+
115
+ it('should return the fallback value on error', async () => {
116
+ const slotId = 'slot-id';
117
+
118
+ const fallback = {
119
+ id: 'fallback',
120
+ };
121
+
122
+ const options: FetchOptions = {
123
+ apiKey: apiKey,
124
+ timeout: 100,
125
+ fallback: fallback,
126
+ };
127
+
128
+ jest.mocked(mockFetch).mockRejectedValue(new Error('error'));
129
+
130
+ await expect(fetchContent(slotId, options)).resolves.toEqual({
131
+ content: fallback,
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,47 @@
1
+ import {
2
+ ContentFetcher,
3
+ DynamicContentOptions as BaseDynamicOptions,
4
+ StaticContentOptions as BaseStaticOptions,
5
+ } from '@croct/sdk/contentFetcher';
6
+ import {JsonObject} from '@croct/plug/sdk/json';
7
+ import {FetchResponse} from '@croct/plug/plug';
8
+ import {SlotContent, VersionedSlotId} from '@croct/plug/slot';
9
+
10
+ type ServerSideOptions<T extends JsonObject> = {
11
+ apiKey: string,
12
+ fallback?: T,
13
+ };
14
+
15
+ export type DynamicContentOptions<T extends JsonObject = JsonObject> =
16
+ Omit<BaseDynamicOptions, 'version'> & ServerSideOptions<T>;
17
+
18
+ export type StaticContentOptions<T extends JsonObject = JsonObject> =
19
+ Omit<BaseStaticOptions, 'version'> & ServerSideOptions<T>;
20
+
21
+ export type FetchOptions<T extends JsonObject = JsonObject> = DynamicContentOptions<T> | StaticContentOptions<T>;
22
+
23
+ export function fetchContent<I extends VersionedSlotId, C extends JsonObject>(
24
+ slotId: I,
25
+ options?: FetchOptions<SlotContent<I, C>>,
26
+ ): Promise<Omit<FetchResponse<I, C>, 'payload'>> {
27
+ const {apiKey, fallback, ...fetchOptions} = options ?? {};
28
+ const [id, version = 'latest'] = slotId.split('@') as [I, `${number}` | 'latest' | undefined];
29
+
30
+ const promise = (new ContentFetcher({apiKey: apiKey}))
31
+ .fetch<SlotContent<I, C>>(
32
+ id,
33
+ version === 'latest'
34
+ ? fetchOptions
35
+ : {...fetchOptions, version: version},
36
+ );
37
+
38
+ if (fallback !== undefined) {
39
+ return promise.catch(
40
+ () => ({
41
+ content: fallback,
42
+ }),
43
+ );
44
+ }
45
+
46
+ return promise;
47
+ }
@@ -0,0 +1,2 @@
1
+ export * from './evaluate';
2
+ export * from './fetchContent';
@@ -0,0 +1,2 @@
1
+ export * from './Personalization';
2
+ export * from './Slot';
@@ -0,0 +1,7 @@
1
+ import {EapFeatures} from '@croct/plug/eap';
2
+
3
+ declare global {
4
+ interface Window {
5
+ croctEap?: Partial<EapFeatures>;
6
+ }
7
+ }
@@ -0,0 +1,280 @@
1
+ import {Cache, EntryOptions} from './Cache';
2
+
3
+ describe('Cache', () => {
4
+ afterEach(() => {
5
+ jest.clearAllTimers();
6
+ jest.resetAllMocks();
7
+ });
8
+
9
+ it('should load and cache the value for the default cache time', async () => {
10
+ jest.useFakeTimers();
11
+
12
+ const cache = new Cache(10);
13
+
14
+ const loader = jest.fn()
15
+ .mockResolvedValueOnce('result1')
16
+ .mockResolvedValueOnce('result2');
17
+
18
+ const options: EntryOptions<string> = {
19
+ cacheKey: 'key',
20
+ loader: loader,
21
+ };
22
+
23
+ let promise: Promise<any>|undefined;
24
+
25
+ try {
26
+ cache.load(options);
27
+ } catch (result: any|undefined) {
28
+ promise = result;
29
+ }
30
+
31
+ await expect(promise).resolves.toEqual('result1');
32
+
33
+ expect(cache.load(options)).toEqual('result1');
34
+
35
+ expect(loader).toHaveBeenCalledTimes(1);
36
+
37
+ jest.advanceTimersByTime(10);
38
+
39
+ try {
40
+ cache.load(options);
41
+ } catch (result: any|undefined) {
42
+ promise = result;
43
+ }
44
+
45
+ await expect(promise).resolves.toEqual('result2');
46
+
47
+ expect(loader).toHaveBeenCalledTimes(2);
48
+ });
49
+
50
+ it('should load the value once before expiration', async () => {
51
+ jest.useFakeTimers();
52
+
53
+ const cache = new Cache(10);
54
+
55
+ const loader = jest.fn(
56
+ () => new Promise<string>(resolve => {
57
+ setTimeout(() => resolve('done'), 10);
58
+ }),
59
+ );
60
+
61
+ const options: EntryOptions<string> = {
62
+ cacheKey: 'key',
63
+ loader: loader,
64
+ };
65
+
66
+ let promise1: Promise<any>|undefined;
67
+
68
+ try {
69
+ cache.load(options);
70
+ } catch (result: any|undefined) {
71
+ promise1 = result;
72
+ }
73
+
74
+ let promise2: Promise<any>|undefined;
75
+
76
+ try {
77
+ cache.load(options);
78
+ } catch (result: any|undefined) {
79
+ promise2 = result;
80
+ }
81
+
82
+ expect(promise1).toBe(promise2);
83
+
84
+ jest.advanceTimersByTime(10);
85
+
86
+ await expect(promise1).resolves.toEqual('done');
87
+ await expect(promise2).resolves.toEqual('done');
88
+
89
+ expect(loader).toHaveBeenCalledTimes(1);
90
+ });
91
+
92
+ it('should load and cache the value for the specified time', async () => {
93
+ jest.useFakeTimers();
94
+
95
+ const cache = new Cache(10);
96
+
97
+ const loader = jest.fn()
98
+ .mockResolvedValueOnce('result1')
99
+ .mockResolvedValueOnce('result2');
100
+
101
+ const options: EntryOptions<string> = {
102
+ cacheKey: 'key',
103
+ loader: loader,
104
+ expiration: 15,
105
+ };
106
+
107
+ let promise: Promise<any>|undefined;
108
+
109
+ try {
110
+ cache.load(options);
111
+ } catch (result: any|undefined) {
112
+ promise = result;
113
+ }
114
+
115
+ await expect(promise).resolves.toEqual('result1');
116
+
117
+ expect(cache.load(options)).toEqual('result1');
118
+
119
+ expect(loader).toHaveBeenCalledTimes(1);
120
+
121
+ jest.advanceTimersByTime(15);
122
+
123
+ try {
124
+ cache.load(options);
125
+ } catch (result: any|undefined) {
126
+ promise = result;
127
+ }
128
+
129
+ await expect(promise).resolves.toEqual('result2');
130
+
131
+ expect(loader).toHaveBeenCalledTimes(2);
132
+ });
133
+
134
+ it('should load and cache the value for undetermined time', async () => {
135
+ jest.useFakeTimers();
136
+
137
+ const cache = new Cache(10);
138
+
139
+ const loader = jest.fn()
140
+ .mockResolvedValueOnce('result1')
141
+ .mockResolvedValueOnce('result2');
142
+
143
+ const options: EntryOptions<string> = {
144
+ cacheKey: 'key',
145
+ loader: loader,
146
+ expiration: -1,
147
+ };
148
+
149
+ let promise: Promise<any>|undefined;
150
+
151
+ try {
152
+ cache.load(options);
153
+ } catch (result: any|undefined) {
154
+ promise = result;
155
+ }
156
+
157
+ await expect(promise).resolves.toEqual('result1');
158
+
159
+ jest.advanceTimersByTime(60_000);
160
+
161
+ expect(cache.load(options)).toEqual('result1');
162
+
163
+ expect(loader).toHaveBeenCalledTimes(1);
164
+ });
165
+
166
+ it('should return the fallback value on error', async () => {
167
+ const cache = new Cache(10);
168
+
169
+ const loader = jest.fn().mockRejectedValue(new Error('failed'));
170
+ const options: EntryOptions<string> = {
171
+ cacheKey: 'key',
172
+ loader: loader,
173
+ fallback: 'fallback',
174
+ };
175
+
176
+ let promise: Promise<any>|undefined;
177
+
178
+ try {
179
+ cache.load(options);
180
+ } catch (result: any|undefined) {
181
+ promise = result;
182
+ }
183
+
184
+ await expect(promise).resolves.toBeUndefined();
185
+
186
+ expect(cache.load(options)).toEqual('fallback');
187
+
188
+ // Should cache the result but not the fallback value
189
+ expect(cache.load({...options, fallback: 'error'})).toEqual('error');
190
+
191
+ expect(loader).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ it('should throw the error if no fallback is specified', async () => {
195
+ const cache = new Cache(10);
196
+
197
+ const error = new Error('failed');
198
+
199
+ const loader = jest.fn().mockRejectedValue(error);
200
+ const options: EntryOptions<string> = {
201
+ cacheKey: 'key',
202
+ loader: loader,
203
+ };
204
+
205
+ let promise: Promise<any>|undefined;
206
+
207
+ try {
208
+ cache.load(options);
209
+ } catch (result: any|undefined) {
210
+ promise = result;
211
+ }
212
+
213
+ await expect(promise).resolves.toBeUndefined();
214
+
215
+ await expect(() => cache.load(options)).toThrow(error);
216
+ });
217
+
218
+ it('should cache the error', async () => {
219
+ const cache = new Cache(10);
220
+
221
+ const error = new Error('error');
222
+ const loader = jest.fn().mockRejectedValue(error);
223
+ const options: EntryOptions<string> = {
224
+ cacheKey: 'key',
225
+ loader: loader,
226
+ };
227
+
228
+ let promise: Promise<any>|undefined;
229
+
230
+ try {
231
+ cache.load(options);
232
+ } catch (result: any|undefined) {
233
+ promise = result;
234
+ }
235
+
236
+ await expect(promise).resolves.toBeUndefined();
237
+
238
+ expect(() => cache.load(options)).toThrow(error);
239
+ expect(cache.get(options.cacheKey)?.error).toBe(error);
240
+ });
241
+
242
+ it('should provide the cached values', async () => {
243
+ jest.useFakeTimers();
244
+
245
+ const cache = new Cache(10);
246
+
247
+ const loader = jest.fn().mockResolvedValue('loaded');
248
+ const options: EntryOptions<string> = {
249
+ cacheKey: 'key',
250
+ loader: loader,
251
+ };
252
+
253
+ let promise: Promise<any>|undefined;
254
+
255
+ try {
256
+ cache.load(options);
257
+ } catch (result: any|undefined) {
258
+ promise = result;
259
+ }
260
+
261
+ await promise;
262
+
263
+ jest.advanceTimersByTime(9);
264
+
265
+ const entry = cache.get(options.cacheKey);
266
+
267
+ expect(entry?.result).toBe('loaded');
268
+ expect(entry?.promise).toBe(promise);
269
+ expect(entry?.timeout).not.toBeUndefined();
270
+ expect(entry?.error).toBeUndefined();
271
+
272
+ entry?.dispose();
273
+
274
+ jest.advanceTimersByTime(9);
275
+
276
+ expect(cache.get(options.cacheKey)).toBe(entry);
277
+
278
+ expect(loader).toHaveBeenCalledTimes(1);
279
+ });
280
+ });
@@ -0,0 +1,97 @@
1
+ export type EntryLoader<R> = (...args: any) => Promise<R>;
2
+
3
+ export type EntryOptions<R> = {
4
+ cacheKey: string,
5
+ loader: EntryLoader<R>,
6
+ fallback?: R,
7
+ expiration?: number,
8
+ };
9
+
10
+ type Entry<R = any> = {
11
+ promise: Promise<any>,
12
+ result?: R,
13
+ dispose: () => void,
14
+ timeout?: number,
15
+ error?: any,
16
+ };
17
+
18
+ export class Cache {
19
+ private readonly cache: Record<string, Entry> = {};
20
+
21
+ private readonly defaultExpiration: number;
22
+
23
+ public constructor(defaultExpiration: number) {
24
+ this.defaultExpiration = defaultExpiration;
25
+ }
26
+
27
+ public load<R>(configuration: EntryOptions<R>): R {
28
+ const {cacheKey, loader, fallback, expiration = this.defaultExpiration} = configuration;
29
+
30
+ const cachedEntry = this.get<R>(cacheKey);
31
+
32
+ if (cachedEntry !== undefined) {
33
+ if (cachedEntry.error !== undefined) {
34
+ if (fallback !== undefined) {
35
+ return fallback;
36
+ }
37
+
38
+ throw cachedEntry.error;
39
+ }
40
+
41
+ if (cachedEntry.result !== undefined) {
42
+ return cachedEntry.result;
43
+ }
44
+
45
+ throw cachedEntry.promise;
46
+ }
47
+
48
+ const entry: Entry<R> = {
49
+ dispose: () => {
50
+ if (entry.timeout !== undefined || expiration < 0) {
51
+ return;
52
+ }
53
+
54
+ entry.timeout = window.setTimeout(
55
+ (): void => {
56
+ delete this.cache[cacheKey];
57
+ },
58
+ expiration,
59
+ );
60
+ },
61
+ promise: loader()
62
+ .then((result): R => {
63
+ entry.result = result;
64
+
65
+ return result;
66
+ })
67
+ .catch(error => {
68
+ entry.error = error;
69
+ })
70
+ .finally(() => {
71
+ entry.dispose();
72
+ }),
73
+ };
74
+
75
+ this.cache[cacheKey] = entry;
76
+
77
+ throw entry.promise;
78
+ }
79
+
80
+ public get<R>(cacheKey: string): Entry<R>|undefined {
81
+ const entry = this.cache[cacheKey];
82
+
83
+ if (entry === undefined) {
84
+ return undefined;
85
+ }
86
+
87
+ if (entry.timeout !== undefined) {
88
+ clearTimeout(entry.timeout);
89
+
90
+ delete entry.timeout;
91
+
92
+ entry.dispose();
93
+ }
94
+
95
+ return entry;
96
+ }
97
+ }
@@ -0,0 +1,3 @@
1
+ export * from './useEvaluation';
2
+ export * from './useContent';
3
+ export * from './useCroct';
@@ -0,0 +1,23 @@
1
+ import {renderHook} from '@testing-library/react';
2
+ import {useContent} from './useContent';
3
+
4
+ jest.mock(
5
+ '../ssr-polyfills',
6
+ () => ({
7
+ __esModule: true,
8
+ isSsr: (): boolean => true,
9
+ }),
10
+ );
11
+
12
+ describe('useContent (SSR)', () => {
13
+ it('should render the initial value on the server-side', () => {
14
+ const {result} = renderHook(() => useContent('slot-id', {initial: 'foo'}));
15
+
16
+ expect(result.current).toBe('foo');
17
+ });
18
+
19
+ it('should require an initial value for server-side rending', () => {
20
+ expect(() => useContent('slot-id'))
21
+ .toThrow(new Error('The initial value is required for server-side rendering (SSR).'));
22
+ });
23
+ });
@@ -0,0 +1,66 @@
1
+ import {renderHook} from '@testing-library/react';
2
+ import {Plug} from '@croct/plug';
3
+ import {useCroct} from './useCroct';
4
+ import {useLoader} from './useLoader';
5
+ import {useContent} from './useContent';
6
+
7
+ jest.mock(
8
+ './useCroct',
9
+ () => ({
10
+ useCroct: jest.fn(),
11
+ }),
12
+ );
13
+
14
+ jest.mock(
15
+ './useLoader',
16
+ () => ({
17
+ useLoader: jest.fn(),
18
+ }),
19
+ );
20
+
21
+ describe('useContent (CSR)', () => {
22
+ it('should evaluate fetch the content', () => {
23
+ const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
24
+ payload: {
25
+ title: 'loaded',
26
+ },
27
+ });
28
+
29
+ jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
30
+ jest.mocked(useLoader).mockReturnValue('foo');
31
+
32
+ const slotId = 'home-banner@1';
33
+
34
+ const {result} = renderHook(
35
+ () => useContent<{title: string}>(slotId, {
36
+ preferredLocale: 'en',
37
+ cacheKey: 'unique',
38
+ fallback: {
39
+ title: 'error',
40
+ },
41
+ expiration: 50,
42
+ }),
43
+ );
44
+
45
+ expect(useCroct).toHaveBeenCalled();
46
+ expect(useLoader).toHaveBeenCalledWith({
47
+ cacheKey: `useContent:unique:${slotId}`,
48
+ fallback: {
49
+ title: 'error',
50
+ },
51
+ expiration: 50,
52
+ loader: expect.any(Function),
53
+ });
54
+
55
+ jest.mocked(useLoader)
56
+ .mock
57
+ .calls[0][0]
58
+ .loader();
59
+
60
+ expect(fetch).toHaveBeenCalledWith(slotId, {
61
+ preferredLocale: 'en',
62
+ });
63
+
64
+ expect(result.current).toBe('foo');
65
+ });
66
+ });