@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.
- package/README.md +96 -102
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +6 -0
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/dist/types/factory.d.ts +0 -4
- package/dist/types/options.d.ts +6 -0
- package/package.json +102 -71
- package/src/factory.ts +0 -4
- package/src/functions/api.ts +19 -6
- package/src/functions/index.ts +1 -1
- package/src/options.ts +7 -0
- package/dist/types/components/intl/index.d.ts +0 -2
- package/dist/types/components/mediaDevices/index.d.ts +0 -2
- package/src/components/canvas/index.test.ts +0 -38
- package/src/components/hardware/index.test.ts +0 -80
- package/src/components/intl/index.test.ts +0 -124
- package/src/components/intl/index.ts +0 -41
- package/src/components/mediaDevices/index.test.ts +0 -120
- package/src/components/mediaDevices/index.ts +0 -26
- package/src/components/system/browser.test.ts +0 -108
- package/src/components/webgl/index.test.ts +0 -223
- package/src/functions/api.test.ts +0 -366
- package/src/functions/filterComponents.test.ts +0 -273
- package/src/functions/functions.test.ts +0 -142
- package/src/functions/metadata.test.ts +0 -211
- package/src/options.test.ts +0 -10
- package/src/thumbmark.custom-components.test.ts +0 -87
- package/src/thumbmark.test.ts +0 -59
- package/src/utils/cache.test.ts +0 -95
- package/src/utils/raceAll.test.ts +0 -86
- package/src/utils/stableStringify.test.ts +0 -335
- 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
|
-
})
|