@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.
- package/CroctProvider.d.ts +7 -7
- package/CroctProvider.js +37 -0
- package/CroctProvider.js.map +1 -0
- package/README.md +245 -107
- package/api/evaluate.d.ts +7 -0
- package/api/evaluate.js +15 -0
- package/api/evaluate.js.map +1 -0
- package/api/fetchContent.d.ts +13 -0
- package/api/fetchContent.js +20 -0
- package/api/fetchContent.js.map +1 -0
- package/api/index.d.ts +2 -0
- package/api/index.js +19 -0
- package/api/index.js.map +1 -0
- package/components/Personalization/index.d.ts +10 -10
- package/components/Personalization/index.js +13 -0
- package/components/Personalization/index.js.map +1 -0
- package/components/Slot/index.d.ts +19 -19
- package/components/Slot/index.js +13 -0
- package/components/Slot/index.js.map +1 -0
- package/components/index.d.ts +2 -2
- package/components/index.js +19 -0
- package/components/index.js.map +1 -0
- package/hooks/Cache.d.ts +22 -22
- package/hooks/Cache.js +62 -0
- package/hooks/Cache.js.map +1 -0
- package/hooks/index.d.ts +3 -3
- package/hooks/index.js +20 -0
- package/hooks/index.js.map +1 -0
- package/hooks/useContent.d.ts +18 -17
- package/hooks/useContent.js +25 -0
- package/hooks/useContent.js.map +1 -0
- package/hooks/useCroct.d.ts +2 -2
- package/hooks/useCroct.js +14 -0
- package/hooks/useCroct.js.map +1 -0
- package/hooks/useEvaluation.d.ts +11 -11
- package/hooks/useEvaluation.js +35 -0
- package/hooks/useEvaluation.js.map +1 -0
- package/hooks/useLoader.d.ts +5 -5
- package/hooks/useLoader.js +41 -0
- package/hooks/useLoader.js.map +1 -0
- package/index.d.ts +5 -3
- package/index.js +20 -337
- package/index.js.map +1 -1
- package/package.json +33 -46
- package/src/api/evaluate.test.ts +59 -0
- package/src/api/evaluate.ts +19 -0
- package/src/api/fetchContent.test.ts +134 -0
- package/src/api/fetchContent.ts +47 -0
- package/src/api/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/src/global.d.ts +7 -0
- package/src/hooks/Cache.test.ts +280 -0
- package/src/hooks/Cache.ts +97 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useContent.ssr.test.ts +23 -0
- package/src/hooks/useContent.test.ts +66 -0
- package/src/hooks/useContent.ts +69 -0
- package/src/hooks/useCroct.ts +13 -0
- package/src/hooks/useEvaluation.ssr.test.ts +23 -0
- package/src/hooks/useEvaluation.test.ts +92 -0
- package/src/hooks/useEvaluation.ts +58 -0
- package/src/hooks/useLoader.test.ts +320 -0
- package/src/hooks/useLoader.ts +50 -0
- package/src/index.ts +5 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/ssr-polyfills.ssr.test.ts +46 -0
- package/src/ssr-polyfills.test.ts +65 -0
- package/src/ssr-polyfills.ts +68 -0
- package/ssr-polyfills.d.ts +3 -3
- package/ssr-polyfills.js +64 -0
- package/ssr-polyfills.js.map +1 -0
- package/CroctProvider.test.d.ts +0 -1
- package/components/Personalization/index.d.test.d.ts +0 -1
- package/components/Personalization/index.stories.d.ts +0 -7
- package/components/Personalization/index.test.d.ts +0 -1
- package/components/Slot/index.d.test.d.ts +0 -1
- package/components/Slot/index.stories.d.ts +0 -17
- package/components/Slot/index.test.d.ts +0 -1
- package/hooks/Cache.test.d.ts +0 -1
- package/hooks/useContent.d.test.d.ts +0 -1
- package/hooks/useContent.ssr.test.d.ts +0 -1
- package/hooks/useContent.stories.d.ts +0 -19
- package/hooks/useContent.test.d.ts +0 -1
- package/hooks/useCroct.ssr.test.d.ts +0 -1
- package/hooks/useCroct.test.d.ts +0 -1
- package/hooks/useEvaluation.d.test.d.ts +0 -1
- package/hooks/useEvaluation.ssr.test.d.ts +0 -1
- package/hooks/useEvaluation.stories.d.ts +0 -8
- package/hooks/useEvaluation.test.d.ts +0 -1
- package/hooks/useLoader.test.d.ts +0 -1
- package/ssr-polyfills.ssr.test.d.ts +0 -1
- package/ssr-polyfills.test.d.ts +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {SlotContent, VersionedSlotId, VersionedSlotMap} from '@croct/plug/slot';
|
|
2
|
+
import {JsonObject} from '@croct/plug/sdk/json';
|
|
3
|
+
import {FetchOptions} from '@croct/plug/plug';
|
|
4
|
+
import {useLoader} from './useLoader';
|
|
5
|
+
import {useCroct} from './useCroct';
|
|
6
|
+
import {isSsr} from '../ssr-polyfills';
|
|
7
|
+
|
|
8
|
+
export type UseContentOptions<I, F> = FetchOptions & {
|
|
9
|
+
fallback?: F,
|
|
10
|
+
initial?: I,
|
|
11
|
+
cacheKey?: string,
|
|
12
|
+
expiration?: number,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function useCsrContent<I, F>(
|
|
16
|
+
id: VersionedSlotId,
|
|
17
|
+
options: UseContentOptions<I, F> = {},
|
|
18
|
+
): SlotContent<VersionedSlotId> | I | F {
|
|
19
|
+
const {fallback, initial, cacheKey, expiration, ...fetchOptions} = options;
|
|
20
|
+
const croct = useCroct();
|
|
21
|
+
|
|
22
|
+
return useLoader({
|
|
23
|
+
cacheKey: `useContent:${cacheKey ?? ''}:${id}`,
|
|
24
|
+
loader: () => croct.fetch(id, fetchOptions).then(({content}) => content),
|
|
25
|
+
initial: initial,
|
|
26
|
+
fallback: fallback,
|
|
27
|
+
expiration: expiration,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function useSsrContent<I, F>(
|
|
32
|
+
_: VersionedSlotId,
|
|
33
|
+
{initial}: UseContentOptions<I, F> = {},
|
|
34
|
+
): SlotContent<VersionedSlotId> | I | F {
|
|
35
|
+
if (initial === undefined) {
|
|
36
|
+
throw new Error('The initial value is required for server-side rendering (SSR).');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return initial;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type UseContentHook = {
|
|
43
|
+
<P extends JsonObject, I = P, F = P>(
|
|
44
|
+
id: keyof VersionedSlotMap extends never ? string : never,
|
|
45
|
+
options?: UseContentOptions<I, F>
|
|
46
|
+
): P | I | F,
|
|
47
|
+
|
|
48
|
+
<S extends VersionedSlotId>(
|
|
49
|
+
id: S,
|
|
50
|
+
options?: UseContentOptions<never, never>
|
|
51
|
+
): SlotContent<S>,
|
|
52
|
+
|
|
53
|
+
<I, S extends VersionedSlotId>(
|
|
54
|
+
id: S,
|
|
55
|
+
options?: UseContentOptions<I, never>
|
|
56
|
+
): SlotContent<S> | I,
|
|
57
|
+
|
|
58
|
+
<F, S extends VersionedSlotId>(
|
|
59
|
+
id: S,
|
|
60
|
+
options?: UseContentOptions<never, F>
|
|
61
|
+
): SlotContent<S> | F,
|
|
62
|
+
|
|
63
|
+
<I, F, S extends VersionedSlotId>(
|
|
64
|
+
id: S,
|
|
65
|
+
options?: UseContentOptions<I, F>
|
|
66
|
+
): SlotContent<S> | I | F,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const useContent: UseContentHook = isSsr() ? useSsrContent : useCsrContent;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {Plug} from '@croct/plug';
|
|
2
|
+
import {useContext} from 'react';
|
|
3
|
+
import {CroctContext} from '../CroctProvider';
|
|
4
|
+
|
|
5
|
+
export function useCroct(): Plug {
|
|
6
|
+
const context = useContext(CroctContext);
|
|
7
|
+
|
|
8
|
+
if (context === null) {
|
|
9
|
+
throw new Error('useCroct() can only be used in the context of a <CroctProvider> component.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return context.plug;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react';
|
|
2
|
+
import {useEvaluation} from './useEvaluation';
|
|
3
|
+
|
|
4
|
+
jest.mock(
|
|
5
|
+
'../ssr-polyfills',
|
|
6
|
+
() => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
isSsr: (): boolean => true,
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe('useEvaluation (SSR)', () => {
|
|
13
|
+
it('should render the initial value on the server-side', () => {
|
|
14
|
+
const {result} = renderHook(() => useEvaluation('location', {initial: 'foo'}));
|
|
15
|
+
|
|
16
|
+
expect(result.current).toBe('foo');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should require an initial value for server-side rending', () => {
|
|
20
|
+
expect(() => useEvaluation('location'))
|
|
21
|
+
.toThrow(new Error('The initial value is required for server-side rendering (SSR).'));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react';
|
|
2
|
+
import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade';
|
|
3
|
+
import {Plug} from '@croct/plug';
|
|
4
|
+
import {useEvaluation} from './useEvaluation';
|
|
5
|
+
import {useCroct} from './useCroct';
|
|
6
|
+
import {useLoader} from './useLoader';
|
|
7
|
+
|
|
8
|
+
jest.mock(
|
|
9
|
+
'./useCroct',
|
|
10
|
+
() => ({
|
|
11
|
+
useCroct: jest.fn(),
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
jest.mock(
|
|
16
|
+
'./useLoader',
|
|
17
|
+
() => ({
|
|
18
|
+
useLoader: jest.fn(),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
describe('useEvaluation', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.resetModules();
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should evaluate a query', () => {
|
|
29
|
+
const evaluationOptions: EvaluationOptions = {
|
|
30
|
+
timeout: 100,
|
|
31
|
+
attributes: {
|
|
32
|
+
foo: 'bar',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const evaluate: Plug['evaluate'] = jest.fn();
|
|
37
|
+
|
|
38
|
+
jest.mocked(useCroct).mockReturnValue({evaluate: evaluate} as Plug);
|
|
39
|
+
jest.mocked(useLoader).mockReturnValue('foo');
|
|
40
|
+
|
|
41
|
+
const query = 'location';
|
|
42
|
+
|
|
43
|
+
const {result} = renderHook(
|
|
44
|
+
() => useEvaluation(query, {
|
|
45
|
+
...evaluationOptions,
|
|
46
|
+
cacheKey: 'unique',
|
|
47
|
+
fallback: 'error',
|
|
48
|
+
expiration: 50,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(useCroct).toHaveBeenCalled();
|
|
53
|
+
expect(useLoader).toHaveBeenCalledWith({
|
|
54
|
+
cacheKey: 'useEvaluation:unique:location:{"foo":"bar"}',
|
|
55
|
+
fallback: 'error',
|
|
56
|
+
expiration: 50,
|
|
57
|
+
loader: expect.any(Function),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
jest.mocked(useLoader)
|
|
61
|
+
.mock
|
|
62
|
+
.calls[0][0]
|
|
63
|
+
.loader();
|
|
64
|
+
|
|
65
|
+
expect(evaluate).toHaveBeenCalledWith(query, evaluationOptions);
|
|
66
|
+
|
|
67
|
+
expect(result.current).toBe('foo');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should remove undefined evaluation options', () => {
|
|
71
|
+
const evaluationOptions: EvaluationOptions = {
|
|
72
|
+
timeout: undefined,
|
|
73
|
+
attributes: undefined,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const evaluate: Plug['evaluate'] = jest.fn();
|
|
77
|
+
|
|
78
|
+
jest.mocked(useCroct).mockReturnValue({evaluate: evaluate} as Plug);
|
|
79
|
+
jest.mocked(useLoader).mockReturnValue('foo');
|
|
80
|
+
|
|
81
|
+
const query = 'location';
|
|
82
|
+
|
|
83
|
+
renderHook(() => useEvaluation(query, evaluationOptions));
|
|
84
|
+
|
|
85
|
+
jest.mocked(useLoader)
|
|
86
|
+
.mock
|
|
87
|
+
.calls[0][0]
|
|
88
|
+
.loader();
|
|
89
|
+
|
|
90
|
+
expect(evaluate).toHaveBeenCalledWith(query, {});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {JsonValue} from '@croct/plug/sdk/json';
|
|
2
|
+
import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade';
|
|
3
|
+
import {useLoader} from './useLoader';
|
|
4
|
+
import {useCroct} from './useCroct';
|
|
5
|
+
import {isSsr} from '../ssr-polyfills';
|
|
6
|
+
|
|
7
|
+
function cleanEvaluationOptions(options: EvaluationOptions): EvaluationOptions {
|
|
8
|
+
const result: EvaluationOptions = {};
|
|
9
|
+
|
|
10
|
+
for (const [key, value] of Object.entries(options)) {
|
|
11
|
+
if (value !== undefined) {
|
|
12
|
+
result[key] = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type UseEvaluationOptions<I, F> = EvaluationOptions & {
|
|
20
|
+
initial?: I,
|
|
21
|
+
fallback?: F,
|
|
22
|
+
cacheKey?: string,
|
|
23
|
+
expiration?: number,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type UseEvaluationHook = <T extends JsonValue, I = T, F = T>(
|
|
27
|
+
query: string,
|
|
28
|
+
options?: UseEvaluationOptions<I, F>,
|
|
29
|
+
) => T | I | F;
|
|
30
|
+
|
|
31
|
+
function useCsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
32
|
+
query: string,
|
|
33
|
+
options: UseEvaluationOptions<I, F> = {},
|
|
34
|
+
): T | I | F {
|
|
35
|
+
const {cacheKey, fallback, initial, expiration, ...evaluationOptions} = options;
|
|
36
|
+
const croct = useCroct();
|
|
37
|
+
|
|
38
|
+
return useLoader<T | I | F>({
|
|
39
|
+
cacheKey: `useEvaluation:${cacheKey ?? ''}:${query}:${JSON.stringify(options.attributes ?? '')}`,
|
|
40
|
+
loader: () => croct.evaluate<T & JsonValue>(query, cleanEvaluationOptions(evaluationOptions)),
|
|
41
|
+
initial: initial,
|
|
42
|
+
fallback: fallback,
|
|
43
|
+
expiration: expiration,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function useSsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
48
|
+
_: string,
|
|
49
|
+
{initial}: UseEvaluationOptions<I, F> = {},
|
|
50
|
+
): T | I | F {
|
|
51
|
+
if (initial === undefined) {
|
|
52
|
+
throw new Error('The initial value is required for server-side rendering (SSR).');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return initial;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const useEvaluation: UseEvaluationHook = isSsr() ? useSsrEvaluation : useCsrEvaluation;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import {act, renderHook, waitFor} from '@testing-library/react';
|
|
2
|
+
import {useLoader} from './useLoader';
|
|
3
|
+
|
|
4
|
+
describe('useLoader', () => {
|
|
5
|
+
const cacheKey = {
|
|
6
|
+
index: 0,
|
|
7
|
+
next: function next(): string {
|
|
8
|
+
this.index++;
|
|
9
|
+
|
|
10
|
+
return this.current();
|
|
11
|
+
},
|
|
12
|
+
current: function current(): string {
|
|
13
|
+
return `key-${this.index}`;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
cacheKey.next();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
jest.clearAllTimers();
|
|
23
|
+
jest.resetAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Needed to use fake timers and promises:
|
|
27
|
+
// https://github.com/testing-library/react-testing-library/issues/244#issuecomment-449461804
|
|
28
|
+
function flushPromises(): Promise<void> {
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it('should return the load the value and cache on success', async () => {
|
|
33
|
+
const loader = jest.fn().mockResolvedValue('foo');
|
|
34
|
+
|
|
35
|
+
const {result, rerender} = renderHook(
|
|
36
|
+
() => useLoader({
|
|
37
|
+
cacheKey: cacheKey.current(),
|
|
38
|
+
loader: loader,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
rerender();
|
|
43
|
+
|
|
44
|
+
await waitFor(() => expect(result.current).toBe('foo'));
|
|
45
|
+
|
|
46
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should load the value and cache on error', async () => {
|
|
50
|
+
const error = new Error('fail');
|
|
51
|
+
const loader = jest.fn().mockRejectedValue(error);
|
|
52
|
+
|
|
53
|
+
const {result, rerender} = renderHook(
|
|
54
|
+
() => useLoader({
|
|
55
|
+
cacheKey: cacheKey.current(),
|
|
56
|
+
fallback: error,
|
|
57
|
+
loader: loader,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
rerender();
|
|
62
|
+
|
|
63
|
+
await waitFor(() => expect(result.current).toBe(error));
|
|
64
|
+
|
|
65
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should reload the value on error', async () => {
|
|
69
|
+
const content = {foo: 'qux'};
|
|
70
|
+
|
|
71
|
+
const loader = jest.fn()
|
|
72
|
+
.mockImplementationOnce(() => {
|
|
73
|
+
throw new Error('fail');
|
|
74
|
+
})
|
|
75
|
+
.mockImplementationOnce(() => Promise.resolve(content));
|
|
76
|
+
|
|
77
|
+
const {result, rerender} = renderHook(
|
|
78
|
+
() => useLoader({
|
|
79
|
+
cacheKey: cacheKey.current(),
|
|
80
|
+
initial: {},
|
|
81
|
+
loader: loader,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await act(flushPromises);
|
|
86
|
+
|
|
87
|
+
rerender();
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(result.current).toBe(content));
|
|
90
|
+
|
|
91
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return the initial state on the initial render', async () => {
|
|
95
|
+
const loader = jest.fn(() => Promise.resolve('loaded'));
|
|
96
|
+
|
|
97
|
+
const {result} = renderHook(
|
|
98
|
+
() => useLoader({
|
|
99
|
+
cacheKey: cacheKey.current(),
|
|
100
|
+
initial: 'loading',
|
|
101
|
+
loader: loader,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(result.current).toBe('loading');
|
|
106
|
+
|
|
107
|
+
await waitFor(() => expect(result.current).toBe('loaded'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should update the initial state with the fallback state on error', async () => {
|
|
111
|
+
const loader = jest.fn().mockRejectedValue(new Error('fail'));
|
|
112
|
+
|
|
113
|
+
const {result} = renderHook(
|
|
114
|
+
() => useLoader({
|
|
115
|
+
cacheKey: cacheKey.current(),
|
|
116
|
+
initial: 'loading',
|
|
117
|
+
fallback: 'error',
|
|
118
|
+
loader: loader,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(result.current).toBe('loading');
|
|
123
|
+
|
|
124
|
+
await waitFor(() => expect(result.current).toBe('error'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return the fallback state on error', async () => {
|
|
128
|
+
const loader = jest.fn().mockRejectedValue(new Error('fail'));
|
|
129
|
+
|
|
130
|
+
const {result} = renderHook(
|
|
131
|
+
() => useLoader({
|
|
132
|
+
cacheKey: cacheKey.current(),
|
|
133
|
+
fallback: 'foo',
|
|
134
|
+
loader: loader,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await waitFor(() => expect(result.current).toBe('foo'));
|
|
139
|
+
|
|
140
|
+
expect(loader).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should extend the cache expiration on every render', async () => {
|
|
144
|
+
jest.useFakeTimers();
|
|
145
|
+
|
|
146
|
+
const loader = jest.fn().mockResolvedValue('foo');
|
|
147
|
+
|
|
148
|
+
const {rerender, unmount} = renderHook(
|
|
149
|
+
() => useLoader({
|
|
150
|
+
cacheKey: cacheKey.current(),
|
|
151
|
+
loader: loader,
|
|
152
|
+
expiration: 15,
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await act(flushPromises);
|
|
157
|
+
|
|
158
|
+
jest.advanceTimersByTime(14);
|
|
159
|
+
|
|
160
|
+
rerender();
|
|
161
|
+
|
|
162
|
+
jest.advanceTimersByTime(14);
|
|
163
|
+
|
|
164
|
+
rerender();
|
|
165
|
+
|
|
166
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
167
|
+
|
|
168
|
+
jest.advanceTimersByTime(15);
|
|
169
|
+
|
|
170
|
+
unmount();
|
|
171
|
+
|
|
172
|
+
renderHook(
|
|
173
|
+
() => useLoader({
|
|
174
|
+
cacheKey: cacheKey.current(),
|
|
175
|
+
loader: loader,
|
|
176
|
+
expiration: 15,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await act(flushPromises);
|
|
181
|
+
|
|
182
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should not expire the cache when the expiration is negative', async () => {
|
|
186
|
+
jest.useFakeTimers();
|
|
187
|
+
|
|
188
|
+
const loader = jest.fn(
|
|
189
|
+
() => new Promise(resolve => {
|
|
190
|
+
setTimeout(() => resolve('foo'), 10);
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const {rerender} = renderHook(
|
|
195
|
+
() => useLoader({
|
|
196
|
+
cacheKey: cacheKey.current(),
|
|
197
|
+
loader: loader,
|
|
198
|
+
expiration: -1,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
jest.advanceTimersByTime(10);
|
|
203
|
+
|
|
204
|
+
await act(flushPromises);
|
|
205
|
+
|
|
206
|
+
// First rerender
|
|
207
|
+
rerender();
|
|
208
|
+
|
|
209
|
+
// Second rerender
|
|
210
|
+
rerender();
|
|
211
|
+
|
|
212
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it.each<[number, number|undefined]>(
|
|
216
|
+
[
|
|
217
|
+
// [Expected elapsed time, Expiration]
|
|
218
|
+
[60_000, undefined],
|
|
219
|
+
[15_000, 15_000],
|
|
220
|
+
],
|
|
221
|
+
)('should cache the values for %d milliseconds', async (step, expiration) => {
|
|
222
|
+
jest.useFakeTimers();
|
|
223
|
+
|
|
224
|
+
const delay = 10;
|
|
225
|
+
const loader = jest.fn(
|
|
226
|
+
() => new Promise(resolve => {
|
|
227
|
+
setTimeout(() => resolve('foo'), delay);
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const {result: firstTime} = renderHook(
|
|
232
|
+
() => useLoader({
|
|
233
|
+
cacheKey: cacheKey.current(),
|
|
234
|
+
expiration: expiration,
|
|
235
|
+
loader: loader,
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
jest.advanceTimersByTime(delay);
|
|
240
|
+
|
|
241
|
+
await act(flushPromises);
|
|
242
|
+
|
|
243
|
+
await waitFor(() => expect(firstTime.current).toBe('foo'));
|
|
244
|
+
|
|
245
|
+
const {result: secondTime} = renderHook(
|
|
246
|
+
() => useLoader({
|
|
247
|
+
cacheKey: cacheKey.current(),
|
|
248
|
+
expiration: expiration,
|
|
249
|
+
loader: loader,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(secondTime.current).toBe('foo');
|
|
254
|
+
|
|
255
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
256
|
+
|
|
257
|
+
jest.advanceTimersByTime(step);
|
|
258
|
+
|
|
259
|
+
const {result: thirdTime} = renderHook(
|
|
260
|
+
() => useLoader({
|
|
261
|
+
cacheKey: cacheKey.current(),
|
|
262
|
+
expiration: expiration,
|
|
263
|
+
loader: loader,
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
jest.advanceTimersByTime(delay);
|
|
268
|
+
|
|
269
|
+
await act(flushPromises);
|
|
270
|
+
|
|
271
|
+
await waitFor(() => expect(thirdTime.current).toBe('foo'));
|
|
272
|
+
|
|
273
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should dispose the cache on unmount', async () => {
|
|
277
|
+
jest.useFakeTimers();
|
|
278
|
+
|
|
279
|
+
const delay = 10;
|
|
280
|
+
const loader = jest.fn(
|
|
281
|
+
() => new Promise(resolve => {
|
|
282
|
+
setTimeout(() => resolve('foo'), delay);
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const {unmount} = renderHook(
|
|
287
|
+
() => useLoader({
|
|
288
|
+
cacheKey: cacheKey.current(),
|
|
289
|
+
expiration: 5,
|
|
290
|
+
loader: loader,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
jest.advanceTimersByTime(delay);
|
|
295
|
+
|
|
296
|
+
await act(flushPromises);
|
|
297
|
+
|
|
298
|
+
unmount();
|
|
299
|
+
|
|
300
|
+
jest.advanceTimersByTime(5);
|
|
301
|
+
|
|
302
|
+
await act(flushPromises);
|
|
303
|
+
|
|
304
|
+
const {result: secondTime} = renderHook(
|
|
305
|
+
() => useLoader({
|
|
306
|
+
cacheKey: cacheKey.current(),
|
|
307
|
+
expiration: 5,
|
|
308
|
+
loader: loader,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
jest.advanceTimersByTime(delay);
|
|
313
|
+
|
|
314
|
+
await act(flushPromises);
|
|
315
|
+
|
|
316
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
317
|
+
|
|
318
|
+
await waitFor(() => expect(secondTime.current).toBe('foo'));
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {Cache, EntryOptions} from './Cache';
|
|
3
|
+
|
|
4
|
+
const cache = new Cache(60 * 1000);
|
|
5
|
+
|
|
6
|
+
export type CacheOptions<R> = EntryOptions<R> & {
|
|
7
|
+
initial?: R,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function useLoader<R>({initial, ...options}: CacheOptions<R>): R {
|
|
11
|
+
const loadedValue: R|undefined = cache.get<R>(options.cacheKey)?.result;
|
|
12
|
+
const [value, setValue] = useState(loadedValue !== undefined ? loadedValue : initial);
|
|
13
|
+
const mountedRef = useRef(true);
|
|
14
|
+
const optionsRef = useRef(initial !== undefined ? options : undefined);
|
|
15
|
+
|
|
16
|
+
useEffect(
|
|
17
|
+
() => {
|
|
18
|
+
if (optionsRef.current !== undefined) {
|
|
19
|
+
try {
|
|
20
|
+
setValue(cache.load(optionsRef.current));
|
|
21
|
+
} catch (result: unknown) {
|
|
22
|
+
if (result instanceof Promise) {
|
|
23
|
+
result.then((resolvedValue: R) => {
|
|
24
|
+
if (mountedRef.current) {
|
|
25
|
+
setValue(resolvedValue);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setValue(undefined);
|
|
33
|
+
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
mountedRef.current = false;
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
[],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return cache.load(options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return value;
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="react-scripts" />
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import croct from '@croct/plug';
|
|
5
|
+
import {croct as croctPolyfill, isSsr} from './ssr-polyfills';
|
|
6
|
+
|
|
7
|
+
jest.mock(
|
|
8
|
+
'@croct/plug',
|
|
9
|
+
() => ({
|
|
10
|
+
plug: jest.fn(),
|
|
11
|
+
unplug: jest.fn(),
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
describe('Croct polyfill (SSR)', () => {
|
|
16
|
+
it('should not plug', () => {
|
|
17
|
+
croctPolyfill.plug({appId: '00000000-0000-0000-0000-000000000000'});
|
|
18
|
+
|
|
19
|
+
expect(croct.plug).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should not unplug', async () => {
|
|
23
|
+
await expect(croctPolyfill.unplug()).resolves.toBeUndefined();
|
|
24
|
+
|
|
25
|
+
expect(croct.unplug).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not initialize', () => {
|
|
29
|
+
expect(croctPolyfill.initialized).toBe(false);
|
|
30
|
+
|
|
31
|
+
croctPolyfill.plug({appId: '00000000-0000-0000-0000-000000000000'});
|
|
32
|
+
|
|
33
|
+
expect(croctPolyfill.initialized).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should not allow accessing properties other than plug or unplug', () => {
|
|
37
|
+
expect(() => croctPolyfill.user)
|
|
38
|
+
.toThrow('Property croct.user is not supported on server-side (SSR).');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('isSsr', () => {
|
|
43
|
+
it('should always return true', () => {
|
|
44
|
+
expect(isSsr()).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|