@thumbmarkjs/thumbmarkjs 1.9.1 → 1.10.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 (36) hide show
  1. package/README.md +96 -102
  2. package/dist/thumbmark.cjs.js +1 -1
  3. package/dist/thumbmark.cjs.js.map +1 -1
  4. package/dist/thumbmark.esm.d.ts +6 -0
  5. package/dist/thumbmark.esm.js +1 -1
  6. package/dist/thumbmark.esm.js.map +1 -1
  7. package/dist/thumbmark.umd.js +1 -1
  8. package/dist/thumbmark.umd.js.map +1 -1
  9. package/dist/types/factory.d.ts +0 -4
  10. package/dist/types/options.d.ts +6 -0
  11. package/package.json +102 -71
  12. package/src/factory.ts +0 -4
  13. package/src/functions/api.ts +19 -6
  14. package/src/functions/index.ts +1 -1
  15. package/src/options.ts +7 -0
  16. package/dist/types/components/intl/index.d.ts +0 -2
  17. package/dist/types/components/mediaDevices/index.d.ts +0 -2
  18. package/src/components/canvas/index.test.ts +0 -38
  19. package/src/components/hardware/index.test.ts +0 -80
  20. package/src/components/intl/index.test.ts +0 -124
  21. package/src/components/intl/index.ts +0 -41
  22. package/src/components/mediaDevices/index.test.ts +0 -120
  23. package/src/components/mediaDevices/index.ts +0 -26
  24. package/src/components/system/browser.test.ts +0 -108
  25. package/src/components/webgl/index.test.ts +0 -223
  26. package/src/functions/api.test.ts +0 -366
  27. package/src/functions/filterComponents.test.ts +0 -273
  28. package/src/functions/functions.test.ts +0 -142
  29. package/src/functions/metadata.test.ts +0 -211
  30. package/src/options.test.ts +0 -10
  31. package/src/thumbmark.custom-components.test.ts +0 -87
  32. package/src/thumbmark.test.ts +0 -59
  33. package/src/utils/cache.test.ts +0 -95
  34. package/src/utils/raceAll.test.ts +0 -86
  35. package/src/utils/stableStringify.test.ts +0 -335
  36. package/src/utils/visitorId.test.ts +0 -102
@@ -1,223 +0,0 @@
1
- import getWebGL, { __resetWebGLCache, __getWebGLCache } from './index';
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mock ImageData — jsdom does not provide a real implementation
5
- // ---------------------------------------------------------------------------
6
- class MockImageData {
7
- data: Uint8ClampedArray;
8
- width: number;
9
- height: number;
10
-
11
- constructor(dataOrWidth: Uint8ClampedArray | number, widthOrUndefined?: number, height?: number) {
12
- if (dataOrWidth instanceof Uint8ClampedArray) {
13
- this.data = dataOrWidth;
14
- this.width = widthOrUndefined ?? 1;
15
- this.height = height ?? 1;
16
- } else {
17
- const w = dataOrWidth as number;
18
- const h = widthOrUndefined ?? 1;
19
- this.data = new Uint8ClampedArray(w * h * 4);
20
- this.width = w;
21
- this.height = h;
22
- }
23
- }
24
- }
25
- (global as any).ImageData = MockImageData;
26
-
27
- // ---------------------------------------------------------------------------
28
- // WebGL mock factory — returns an object satisfying setupWebGL() and renderImage()
29
- // ---------------------------------------------------------------------------
30
- function makeGlMock(overrides: Partial<WebGLRenderingContext> = {}): WebGLRenderingContext {
31
- const gl: any = {
32
- VERTEX_SHADER: 35633,
33
- FRAGMENT_SHADER: 35632,
34
- ARRAY_BUFFER: 34962,
35
- COMPILE_STATUS: 35713,
36
- LINK_STATUS: 35714,
37
- RGBA: 6408,
38
- UNSIGNED_BYTE: 5121,
39
- COLOR_BUFFER_BIT: 16384,
40
- FLOAT: 5126,
41
- LINES: 1,
42
- drawingBufferWidth: 200,
43
- drawingBufferHeight: 100,
44
- createShader: jest.fn().mockReturnValue({}),
45
- shaderSource: jest.fn(),
46
- compileShader: jest.fn(),
47
- getShaderParameter: jest.fn().mockReturnValue(true),
48
- createProgram: jest.fn().mockReturnValue({}),
49
- attachShader: jest.fn(),
50
- linkProgram: jest.fn(),
51
- getProgramParameter: jest.fn().mockReturnValue(true),
52
- createBuffer: jest.fn().mockReturnValue({}),
53
- bindBuffer: jest.fn(),
54
- bufferData: jest.fn(),
55
- useProgram: jest.fn(),
56
- getAttribLocation: jest.fn().mockReturnValue(0),
57
- enableVertexAttribArray: jest.fn(),
58
- vertexAttribPointer: jest.fn(),
59
- viewport: jest.fn(),
60
- clearColor: jest.fn(),
61
- clear: jest.fn(),
62
- drawArrays: jest.fn(),
63
- readPixels: jest.fn(),
64
- ...overrides,
65
- };
66
- return gl as WebGLRenderingContext;
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // Suite setup
71
- // ---------------------------------------------------------------------------
72
- let originalGetContext: any;
73
-
74
- beforeAll(() => {
75
- originalGetContext = HTMLCanvasElement.prototype.getContext;
76
- });
77
-
78
- afterAll(() => {
79
- HTMLCanvasElement.prototype.getContext = originalGetContext;
80
- });
81
-
82
- beforeEach(() => {
83
- __resetWebGLCache();
84
- });
85
-
86
- // ---------------------------------------------------------------------------
87
- // Tests
88
- // ---------------------------------------------------------------------------
89
- describe('webgl component — context-loss recovery', () => {
90
-
91
- // Happy path: cache is populated after first successful call
92
- test('first getWebGL() call populates the cache', async () => {
93
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
94
-
95
- await getWebGL();
96
-
97
- expect(__getWebGLCache()).not.toBeNull();
98
- });
99
-
100
- // Cache reuse: two calls share the same canvas object
101
- test('two consecutive getWebGL() calls reuse the same cached canvas', async () => {
102
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
103
-
104
- await getWebGL();
105
- const cacheAfterFirst = __getWebGLCache();
106
- expect(cacheAfterFirst).not.toBeNull();
107
-
108
- await getWebGL();
109
- const cacheAfterSecond = __getWebGLCache();
110
-
111
- // Same object reference — no new canvas was created
112
- expect(cacheAfterSecond).toBe(cacheAfterFirst);
113
- // getContext called exactly once (canvas created once)
114
- expect(HTMLCanvasElement.prototype.getContext).toHaveBeenCalledTimes(1);
115
- });
116
-
117
- // Context-loss listener: dispatching webglcontextlost clears the cache
118
- test('webglcontextlost event clears the cache', async () => {
119
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
120
-
121
- await getWebGL();
122
- const cacheBeforeLoss = __getWebGLCache();
123
- expect(cacheBeforeLoss).not.toBeNull();
124
-
125
- // Fire the context-lost event on the cached canvas
126
- cacheBeforeLoss!.canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
127
-
128
- expect(__getWebGLCache()).toBeNull();
129
- });
130
-
131
- // Context-loss listener: event.preventDefault() is called
132
- test('webglcontextlost listener calls event.preventDefault()', async () => {
133
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
134
-
135
- await getWebGL();
136
- const cache = __getWebGLCache();
137
- expect(cache).not.toBeNull();
138
-
139
- const lostEvent = new Event('webglcontextlost', { cancelable: true });
140
- const preventDefaultSpy = jest.spyOn(lostEvent, 'preventDefault');
141
-
142
- cache!.canvas.dispatchEvent(lostEvent);
143
-
144
- expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
145
- });
146
-
147
- // After context loss, next getWebGL() call creates a NEW canvas
148
- test('getWebGL() after context loss rebuilds the cache via a new canvas', async () => {
149
- const gl = makeGlMock();
150
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(gl) as any;
151
-
152
- await getWebGL();
153
- const firstCache = __getWebGLCache();
154
- expect(firstCache).not.toBeNull();
155
-
156
- // Simulate context loss
157
- firstCache!.canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
158
- expect(__getWebGLCache()).toBeNull();
159
-
160
- // Reset mock call count, re-install for rebuild
161
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(gl) as any;
162
-
163
- await getWebGL();
164
- const secondCache = __getWebGLCache();
165
- expect(secondCache).not.toBeNull();
166
- // A fresh cache object is created — different reference from the lost one
167
- expect(secondCache).not.toBe(firstCache);
168
- // getContext called once for the rebuild (new canvas)
169
- expect(HTMLCanvasElement.prototype.getContext).toHaveBeenCalledTimes(1);
170
- });
171
-
172
- // once: true — the listener fires at most once per canvas
173
- test('webglcontextlost listener fires only once (once:true)', async () => {
174
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
175
-
176
- await getWebGL();
177
- const firstCache = __getWebGLCache();
178
- const canvas = firstCache!.canvas;
179
-
180
- // First dispatch: clears cache
181
- canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
182
- expect(__getWebGLCache()).toBeNull();
183
-
184
- // Second dispatch on the same canvas — listener already removed (once:true),
185
- // so _cache remains null (unchanged) and no throw occurs
186
- expect(() => {
187
- canvas.dispatchEvent(new Event('webglcontextlost', { cancelable: true }));
188
- }).not.toThrow();
189
- expect(__getWebGLCache()).toBeNull();
190
- });
191
-
192
- // renderImage catch block: render failure clears the cache
193
- test('render failure (useProgram throws) clears the cache', async () => {
194
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(makeGlMock()) as any;
195
-
196
- // First call populates cache
197
- await getWebGL();
198
- expect(__getWebGLCache()).not.toBeNull();
199
-
200
- // Patch the cached gl to throw on the next render
201
- const cache = __getWebGLCache()!;
202
- (cache as any).gl.useProgram = jest.fn().mockImplementation(() => {
203
- throw new Error('WebGL context lost');
204
- });
205
-
206
- // The call should not throw; it should return the 'unsupported' fallback
207
- const result = await getWebGL();
208
- expect(result).toEqual({ webgl: 'unsupported' });
209
-
210
- // Cache must have been cleared by the catch block
211
- expect(__getWebGLCache()).toBeNull();
212
- });
213
-
214
- // Graceful degradation: getContext returns null → returns 'unsupported'
215
- test('getWebGL() returns unsupported when WebGL context is unavailable', async () => {
216
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(null) as any;
217
-
218
- const result = await getWebGL();
219
-
220
- expect(result).toEqual({ webgl: 'unsupported' });
221
- expect(__getWebGLCache()).toBeNull();
222
- });
223
- });
@@ -1,366 +0,0 @@
1
- import { apiResponse, getCachedApiResponse, setCachedApiResponse, getApiPromise } from "./api";
2
- import { defaultOptions } from "../options";
3
- import { getCache, setCache } from "../utils/cache";
4
- import { setVisitorId } from "../utils/visitorId";
5
- import type { componentInterface } from "../factory";
6
- import * as hashModule from "../utils/hash";
7
- import * as stringifyModule from "../utils/stableStringify";
8
-
9
- const options = {
10
- ...defaultOptions,
11
- cache_lifetime_in_ms: 100,
12
- }
13
-
14
- const apiResponse = {
15
- identifier: 'test'
16
- } as apiResponse;
17
-
18
- describe('setCachedApiResponse', () => {
19
- beforeEach(() => {
20
- localStorage.clear();
21
- })
22
-
23
- test('it should write the apiResponse to the cache if options allow that', () => {
24
- setCachedApiResponse(options, apiResponse);
25
- expect(getCache(options).apiResponse).toEqual(apiResponse);
26
- });
27
-
28
- test('it should not write if cache if off', () => {
29
- setCachedApiResponse({
30
- ...options,
31
- cache_api_call: false,
32
- }, apiResponse);
33
-
34
- expect(getCache(options).apiResponse).not.toBeDefined();
35
- expect(getCache(options).apiResponseExpiry).not.toBeDefined();
36
- });
37
-
38
- test('it should not write if lifetime is 0', () => {
39
- setCachedApiResponse({
40
- ...options,
41
- cache_lifetime_in_ms: 0
42
- }, apiResponse);
43
-
44
- expect(getCache(options).apiResponse).not.toBeDefined();
45
- expect(getCache(options).apiResponseExpiry).not.toBeDefined();
46
- });
47
- })
48
-
49
- describe('getCachedApiResponse', () => {
50
- beforeEach(() => {
51
- localStorage.clear();
52
- })
53
-
54
- test('it should get from the cache if a value exists there', () => {
55
- setCache(options, {
56
- apiResponseExpiry: Date.now() + 2000000,
57
- apiResponse,
58
- });
59
-
60
- const cached = getCachedApiResponse(options);
61
- expect(cached).toBeDefined();
62
- expect(cached).toEqual(apiResponse);
63
- });
64
-
65
- test('it should not return an expiried cache', () => {
66
- setCache(options, {
67
- apiResponseExpiry: Date.now() - 2000,
68
- apiResponse,
69
- });
70
-
71
- const cached = getCachedApiResponse(options);
72
- expect(cached).not.toBeDefined();
73
- })
74
-
75
- })
76
-
77
- describe('getApiPromise timeout behavior', () => {
78
- let mockFetch: jest.Mock;
79
- const testOptions = {
80
- ...defaultOptions,
81
- api_key: 'test-api-key',
82
- timeout: 100, // Short timeout for tests
83
- cache_lifetime_in_ms: 1000,
84
- };
85
-
86
- const testComponents: componentInterface = {
87
- userAgent: 'test-agent',
88
- screen: { width: 1920, height: 1080 },
89
- };
90
-
91
- beforeAll(() => {
92
- // Polyfill TextEncoder for jsdom environment
93
- if (typeof TextEncoder === 'undefined') {
94
- const util = require('util');
95
- global.TextEncoder = util.TextEncoder;
96
- global.TextDecoder = util.TextDecoder;
97
- }
98
-
99
- // Mock hash and stableStringify to avoid TextEncoder issues
100
- jest.spyOn(hashModule, 'hash').mockReturnValue('mocked-hash');
101
- jest.spyOn(stringifyModule, 'stableStringify').mockReturnValue('{"mocked":"data"}');
102
-
103
- jest.useFakeTimers();
104
- });
105
-
106
- afterAll(() => {
107
- jest.useRealTimers();
108
- });
109
-
110
- beforeEach(async () => {
111
- // Clear all pending timers and promises from previous tests
112
- jest.clearAllTimers();
113
-
114
- localStorage.clear();
115
- mockFetch = jest.fn();
116
- global.fetch = mockFetch;
117
-
118
- // Re-mock hash and stableStringify for each test
119
- jest.spyOn(hashModule, 'hash').mockReturnValue('mocked-hash');
120
- jest.spyOn(stringifyModule, 'stableStringify').mockReturnValue('{"mocked":"data"}');
121
- });
122
-
123
- afterEach(() => {
124
- jest.restoreAllMocks();
125
- });
126
-
127
- test('returns fetch response when fetch completes before timeout', async () => {
128
- // Use options without caching to avoid state interference
129
- const noCacheOptions = {
130
- ...testOptions,
131
- cache_lifetime_in_ms: 0, // Disable caching
132
- };
133
-
134
- // Mock fetch to resolve quickly
135
- mockFetch.mockResolvedValueOnce({
136
- ok: true,
137
- status: 200,
138
- json: async () => ({
139
- version: '1.2.3',
140
- visitorId: 'test-visitor-id',
141
- thumbmark: 'test-thumbmark',
142
- requestId: 'test-request-id',
143
- info: {
144
- ip_address: {
145
- ip_address: '1.2.3.4',
146
- ip_identifier: 'abc123',
147
- autonomous_system_number: 12345,
148
- ip_version: 'v4' as const,
149
- }
150
- }
151
- })
152
- });
153
-
154
- const promise = getApiPromise(noCacheOptions, testComponents);
155
-
156
- // Fast forward time, but fetch completes before timeout
157
- await jest.runAllTimersAsync();
158
- const result = await promise;
159
-
160
- expect(result).toBeDefined();
161
- expect(result?.version).toBe('1.2.3');
162
- expect(result?.info?.timed_out).toBeUndefined();
163
- expect(result?.thumbmark).toBe('test-thumbmark');
164
- expect(result?.requestId).toBe('test-request-id');
165
- });
166
-
167
- test('returns expired cache when timeout occurs and cache exists', async () => {
168
- // Disable caching to test timeout fallback without request deduplication
169
- const noCacheOptions = {
170
- ...testOptions,
171
- cache_api_call: false,
172
- };
173
-
174
- // Set up expired cache
175
- const expiredCachedValue = {
176
- apiResponseExpiry: Date.now() - 100, // Expired
177
- apiResponse: {
178
- version: '1.2.3',
179
- thumbmark: 'cached-thumbmark',
180
- info: {
181
- ip_address: {
182
- ip_address: '1.2.3.4',
183
- ip_identifier: 'abc123',
184
- autonomous_system_number: 12345,
185
- ip_version: 'v4' as const,
186
- }
187
- }
188
- }
189
- };
190
- setCache(noCacheOptions, expiredCachedValue);
191
-
192
- // Mock fetch to never resolve (hanging request)
193
- mockFetch.mockReturnValueOnce(new Promise(() => { }));
194
-
195
- const promise = getApiPromise(noCacheOptions, testComponents);
196
-
197
- // Advance timers to trigger timeout
198
- await jest.runAllTimersAsync();
199
- const result = await promise;
200
-
201
- // Should return expired cache, not timeout response
202
- expect(result).toBeDefined();
203
- expect(result?.version).toBe('1.2.3');
204
- expect(result?.thumbmark).toBe('cached-thumbmark');
205
- expect(result?.info?.timed_out).toBeUndefined();
206
- });
207
-
208
- test('returns timeout response when timeout occurs and no cache exists', async () => {
209
- const noCacheOptions = {
210
- ...testOptions,
211
- cache_api_call: false,
212
- };
213
-
214
- expect(getCache(noCacheOptions)).toEqual({});
215
-
216
- mockFetch.mockReturnValueOnce(new Promise(() => { }));
217
-
218
- const promise = getApiPromise(noCacheOptions, testComponents);
219
-
220
- await jest.runAllTimersAsync();
221
- const result = await promise;
222
-
223
- expect(result).toBeDefined();
224
- expect(result?.info?.timed_out).toBe(true);
225
-
226
- const cached = getCache(noCacheOptions);
227
- expect(cached.apiResponse).toBeUndefined();
228
- });
229
-
230
- test('returns timeout response without visitorId when no cache and no stored visitorId', async () => {
231
- const noCacheOptions = {
232
- ...testOptions,
233
- cache_api_call: false,
234
- };
235
-
236
- // Ensure no cache and no visitor ID
237
- expect(getCache(noCacheOptions)).toEqual({});
238
- expect(localStorage.getItem('thumbmark_visitor_id')).toBeNull();
239
-
240
- mockFetch.mockReturnValueOnce(new Promise(() => { }));
241
-
242
- const promise = getApiPromise(noCacheOptions, testComponents);
243
-
244
- await jest.runAllTimersAsync();
245
- const result = await promise;
246
-
247
- expect(result).toBeDefined();
248
- expect(result?.info?.timed_out).toBe(true);
249
- expect(result?.visitorId).toBeUndefined();
250
- });
251
-
252
- test('returns timeout response with visitorId when no cache but visitorId exists in localStorage', async () => {
253
- const noCacheOptions = {
254
- ...testOptions,
255
- cache_api_call: false,
256
- };
257
-
258
- // Set up visitor ID in localStorage (no cache)
259
- const storedVisitorId = 'stored-visitor-id-123';
260
- setVisitorId(storedVisitorId, noCacheOptions);
261
-
262
- // Verify setup: no cache, but visitor ID exists
263
- expect(getCache(noCacheOptions)).toEqual({});
264
- expect(localStorage.getItem('thumbmark_visitor_id')).toBe(storedVisitorId);
265
-
266
- mockFetch.mockReturnValueOnce(new Promise(() => { }));
267
-
268
- const promise = getApiPromise(noCacheOptions, testComponents);
269
-
270
- await jest.runAllTimersAsync();
271
- const result = await promise;
272
-
273
- expect(result).toBeDefined();
274
- expect(result?.info?.timed_out).toBe(true);
275
- expect(result?.visitorId).toBe(storedVisitorId);
276
- });
277
-
278
- test('retries on network error and succeeds on second attempt', async () => {
279
- jest.useRealTimers();
280
- const noCacheOptions = { ...testOptions, cache_api_call: false, timeout: 5000 };
281
-
282
- mockFetch
283
- .mockRejectedValueOnce(new TypeError('Failed to fetch'))
284
- .mockResolvedValueOnce({
285
- ok: true, status: 200,
286
- json: async () => ({ version: '1.0', thumbmark: 'retry-ok' }),
287
- });
288
-
289
- const result = await getApiPromise(noCacheOptions, testComponents);
290
-
291
- expect(mockFetch).toHaveBeenCalledTimes(2);
292
- expect(result?.thumbmark).toBe('retry-ok');
293
- jest.useFakeTimers();
294
- });
295
-
296
- test('does not retry on 500 server error', async () => {
297
- jest.useRealTimers();
298
- const noCacheOptions = { ...testOptions, cache_api_call: false, timeout: 5000 };
299
-
300
- mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
301
-
302
- await expect(getApiPromise(noCacheOptions, testComponents)).rejects.toThrow('HTTP error! status: 500');
303
- expect(mockFetch).toHaveBeenCalledTimes(1);
304
- jest.useFakeTimers();
305
- });
306
-
307
- test('does not retry on 403 auth error', async () => {
308
- jest.useRealTimers();
309
- const noCacheOptions = { ...testOptions, cache_api_call: false, timeout: 5000 };
310
-
311
- mockFetch.mockResolvedValueOnce({ ok: false, status: 403 });
312
-
313
- await expect(getApiPromise(noCacheOptions, testComponents)).rejects.toThrow('HTTP error! status: 403');
314
- expect(mockFetch).toHaveBeenCalledTimes(1);
315
- jest.useFakeTimers();
316
- });
317
-
318
- test('network errors exhaust retries then throw', async () => {
319
- jest.useRealTimers();
320
- const noCacheOptions = { ...testOptions, cache_api_call: false, timeout: 5000 };
321
-
322
- mockFetch.mockRejectedValue(new TypeError('Failed to fetch'));
323
-
324
- await expect(getApiPromise(noCacheOptions, testComponents)).rejects.toThrow('Failed to fetch');
325
- expect(mockFetch).toHaveBeenCalledTimes(3);
326
- jest.useFakeTimers();
327
- });
328
-
329
- test('returns full cached response (not just visitorId) when cache exists on timeout', async () => {
330
- const noCacheOptions = {
331
- ...testOptions,
332
- cache_api_call: false,
333
- };
334
-
335
- // Set up both: expired cache AND visitor ID in localStorage
336
- const storedVisitorId = 'stored-visitor-id-456';
337
- setVisitorId(storedVisitorId, noCacheOptions);
338
-
339
- const expiredCachedValue = {
340
- apiResponseExpiry: Date.now() - 100, // Expired
341
- apiResponse: {
342
- version: '1.2.3',
343
- thumbmark: 'cached-thumbmark',
344
- visitorId: 'cached-visitor-id',
345
- info: {
346
- uniqueness: { score: 0.95 }
347
- }
348
- }
349
- };
350
- setCache(noCacheOptions, expiredCachedValue);
351
-
352
- mockFetch.mockReturnValueOnce(new Promise(() => { }));
353
-
354
- const promise = getApiPromise(noCacheOptions, testComponents);
355
-
356
- await jest.runAllTimersAsync();
357
- const result = await promise;
358
-
359
- // Should return full cached response, not just visitor ID fallback
360
- expect(result).toBeDefined();
361
- expect(result?.info?.timed_out).toBeUndefined();
362
- expect(result?.thumbmark).toBe('cached-thumbmark');
363
- expect(result?.visitorId).toBe('cached-visitor-id'); // From cache, not localStorage
364
- expect(result?.version).toBe('1.2.3');
365
- });
366
- })