@thumbmarkjs/thumbmarkjs 1.9.0 → 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/components/audio/index.d.ts +2 -0
- package/dist/types/components/canvas/index.d.ts +3 -0
- package/dist/types/components/fonts/index.d.ts +4 -0
- package/dist/types/components/hardware/index.d.ts +2 -0
- package/dist/types/components/locales/index.d.ts +2 -0
- package/dist/types/components/math/index.d.ts +2 -0
- package/dist/types/components/mathml/index.d.ts +2 -0
- package/dist/types/components/permissions/index.d.ts +3 -0
- package/dist/types/components/plugins/index.d.ts +2 -0
- package/dist/types/components/screen/index.d.ts +2 -0
- package/dist/types/components/speech/index.d.ts +2 -0
- package/dist/types/components/system/browser.d.ts +9 -0
- package/dist/types/components/system/index.d.ts +2 -0
- package/dist/types/components/webgl/index.d.ts +13 -0
- package/dist/types/components/webrtc/index.d.ts +3 -0
- package/dist/types/factory.d.ts +62 -0
- package/dist/types/functions/api.d.ts +73 -0
- package/dist/types/functions/filterComponents.d.ts +15 -0
- package/dist/types/functions/index.d.ts +63 -0
- package/dist/types/functions/legacy_functions.d.ts +27 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/options.d.ts +82 -0
- package/dist/types/thumbmark.d.ts +28 -0
- package/dist/types/utils/cache.d.ts +23 -0
- package/dist/types/utils/commonPixels.d.ts +1 -0
- package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
- package/dist/types/utils/getMostFrequent.d.ts +5 -0
- package/dist/types/utils/hash.d.ts +5 -0
- package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
- package/dist/types/utils/log.d.ts +9 -0
- package/dist/types/utils/raceAll.d.ts +10 -0
- package/dist/types/utils/sort.d.ts +8 -0
- package/dist/types/utils/stableStringify.d.ts +22 -0
- package/dist/types/utils/version.d.ts +4 -0
- package/dist/types/utils/visitorId.d.ts +14 -0
- package/package.json +102 -70
- package/src/factory.ts +0 -4
- package/src/functions/api.ts +19 -6
- package/src/functions/filterComponents.ts +2 -3
- package/src/functions/index.ts +1 -1
- package/src/options.ts +7 -0
- 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 -238
- 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,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
|
-
})
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { getExcludeList, filterThumbmarkData } from './filterComponents';
|
|
2
|
-
import { defaultOptions } from '../options';
|
|
3
|
-
import { componentInterface } from '../factory';
|
|
4
|
-
|
|
5
|
-
// Mock getBrowser so we can control browser detection in tests
|
|
6
|
-
jest.mock('../components/system/browser', () => ({
|
|
7
|
-
getBrowser: jest.fn(() => ({ name: 'unknown', version: 'unknown' })),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
import { getBrowser } from '../components/system/browser';
|
|
11
|
-
const mockGetBrowser = getBrowser as jest.Mock;
|
|
12
|
-
|
|
13
|
-
const testData: componentInterface = {
|
|
14
|
-
one: '1',
|
|
15
|
-
two: 2,
|
|
16
|
-
three: { a: true, b: false },
|
|
17
|
-
speech: { hash: 'abc' },
|
|
18
|
-
canvas: { hash: 'def' },
|
|
19
|
-
audio: { sampleHash: 'ghi', other: 'jkl' },
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// ── getExcludeList ──────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
describe('getExcludeList', () => {
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('returns empty when no options and unknown browser', () => {
|
|
30
|
-
expect(getExcludeList()).toEqual([]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('returns user exclude list as-is', () => {
|
|
34
|
-
const result = getExcludeList({ ...defaultOptions, exclude: ['one', 'two'] });
|
|
35
|
-
expect(result).toContain('one');
|
|
36
|
-
expect(result).toContain('two');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("'always' rules apply even with empty stabilize", () => {
|
|
40
|
-
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '130.0' });
|
|
41
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: [] });
|
|
42
|
-
expect(result).toContain('speech');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("'always' rules don't apply when browser doesn't match", () => {
|
|
46
|
-
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
|
|
47
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: [] });
|
|
48
|
-
expect(result).not.toContain('speech');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('stabilization rules expand for matching browser', () => {
|
|
52
|
-
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
|
|
53
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
|
|
54
|
-
expect(result).toContain('canvas');
|
|
55
|
-
expect(result).toContain('fonts');
|
|
56
|
-
expect(result).toContain('tls.extensions');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('stabilization rules do not apply for non-matching browser', () => {
|
|
60
|
-
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
|
|
61
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
|
|
62
|
-
expect(result).not.toContain('canvas');
|
|
63
|
-
expect(result).not.toContain('fonts');
|
|
64
|
-
// Chrome does get some rules though
|
|
65
|
-
expect(result).toContain('header.acceptLanguage');
|
|
66
|
-
expect(result).toContain('tls.extensions');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('version matching with >= syntax', () => {
|
|
70
|
-
// safari>=17 should match Safari 18
|
|
71
|
-
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '18.0' });
|
|
72
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
|
|
73
|
-
expect(result).toContain('canvas');
|
|
74
|
-
|
|
75
|
-
// safari>=17 should match Safari 17
|
|
76
|
-
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '17.0' });
|
|
77
|
-
const result17 = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
|
|
78
|
-
expect(result17).toContain('canvas');
|
|
79
|
-
|
|
80
|
-
// safari>=17 should NOT match Safari 16
|
|
81
|
-
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '16.5' });
|
|
82
|
-
const result16 = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
|
|
83
|
-
expect(result16).not.toContain('canvas');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('browser-independent rules always apply', () => {
|
|
87
|
-
// 'iframe' has { exclude: ['permissions'] } with no browsers key
|
|
88
|
-
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
|
|
89
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['iframe'] });
|
|
90
|
-
expect(result).toContain('permissions');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('vpn stabilization excludes ip for any browser', () => {
|
|
94
|
-
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
|
|
95
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['vpn'] });
|
|
96
|
-
expect(result).toContain('ip');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('multiple stabilization options combine', () => {
|
|
100
|
-
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
|
|
101
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private', 'vpn'] });
|
|
102
|
-
expect(result).toContain('canvas'); // from private+firefox
|
|
103
|
-
expect(result).toContain('ip'); // from vpn
|
|
104
|
-
expect(result).toContain('speech'); // from always+firefox
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('user exclude list is combined with stabilization rules', () => {
|
|
108
|
-
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
|
|
109
|
-
const result = getExcludeList({ ...defaultOptions, exclude: ['custom'], stabilize: ['private'] });
|
|
110
|
-
expect(result).toContain('custom'); // user's
|
|
111
|
-
expect(result).toContain('canvas'); // from rules
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('unknown stabilization option is silently skipped', () => {
|
|
115
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['nonexistent' as any] });
|
|
116
|
-
// Should not throw, just return whatever 'always' contributes (nothing for unknown browser)
|
|
117
|
-
expect(Array.isArray(result)).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('browser fallback from component data (server-side)', () => {
|
|
121
|
-
// getBrowser returns unknown (simulating server-side)
|
|
122
|
-
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
|
|
123
|
-
|
|
124
|
-
const obj: componentInterface = {
|
|
125
|
-
system: {
|
|
126
|
-
browser: {
|
|
127
|
-
name: 'Brave',
|
|
128
|
-
version: '130.0',
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] }, obj);
|
|
134
|
-
// Brave-specific rules should apply via fallback
|
|
135
|
-
expect(result).toContain('canvas');
|
|
136
|
-
expect(result).toContain('plugins');
|
|
137
|
-
expect(result).toContain('speech'); // from 'always'
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('browser fallback is skipped when getBrowser succeeds', () => {
|
|
141
|
-
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
|
|
142
|
-
|
|
143
|
-
const obj: componentInterface = {
|
|
144
|
-
system: {
|
|
145
|
-
browser: {
|
|
146
|
-
name: 'Firefox',
|
|
147
|
-
version: '120.0',
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] }, obj);
|
|
153
|
-
// Should use Chrome rules (from getBrowser), not Firefox (from obj)
|
|
154
|
-
expect(result).toContain('header.acceptLanguage'); // Chrome rule
|
|
155
|
-
expect(result).not.toContain('fonts'); // Firefox-only rule
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('browser fallback handles missing system gracefully', () => {
|
|
159
|
-
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
|
|
160
|
-
const obj: componentInterface = { one: '1' };
|
|
161
|
-
// Should not throw
|
|
162
|
-
expect(() => getExcludeList({ ...defaultOptions }, obj)).not.toThrow();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// ── filterThumbmarkData ─────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
describe('filterThumbmarkData', () => {
|
|
169
|
-
beforeEach(() => {
|
|
170
|
-
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('returns full object with no options', () => {
|
|
174
|
-
const result = filterThumbmarkData(testData);
|
|
175
|
-
expect(result).toEqual(testData);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('returns full object with empty exclude', () => {
|
|
179
|
-
const result = filterThumbmarkData(testData, { ...defaultOptions, exclude: [], stabilize: [] });
|
|
180
|
-
expect(result).toEqual(testData);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('include overrides exclude for the same key', () => {
|
|
184
|
-
const result = filterThumbmarkData(testData, {
|
|
185
|
-
...defaultOptions,
|
|
186
|
-
exclude: ['one'],
|
|
187
|
-
include: ['one'],
|
|
188
|
-
stabilize: [],
|
|
189
|
-
});
|
|
190
|
-
expect(result.one).toBe('1');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test('include overrides exclude at nested level', () => {
|
|
194
|
-
const result = filterThumbmarkData(testData, {
|
|
195
|
-
...defaultOptions,
|
|
196
|
-
exclude: ['three'],
|
|
197
|
-
include: ['three.a'],
|
|
198
|
-
stabilize: [],
|
|
199
|
-
});
|
|
200
|
-
expect(result.three).toEqual({ a: true });
|
|
201
|
-
expect((result.three as componentInterface).b).toBeUndefined();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('excluding a parent removes all children', () => {
|
|
205
|
-
const result = filterThumbmarkData(testData, {
|
|
206
|
-
...defaultOptions,
|
|
207
|
-
exclude: ['three'],
|
|
208
|
-
stabilize: [],
|
|
209
|
-
});
|
|
210
|
-
expect(result.three).toBeUndefined();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('stabilization rules integrate with filterThumbmarkData', () => {
|
|
214
|
-
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
|
|
215
|
-
const result = filterThumbmarkData(testData, {
|
|
216
|
-
...defaultOptions,
|
|
217
|
-
stabilize: ['private'],
|
|
218
|
-
});
|
|
219
|
-
// Brave+private excludes canvas
|
|
220
|
-
expect(result.canvas).toBeUndefined();
|
|
221
|
-
// Brave+'always' excludes speech
|
|
222
|
-
expect(result.speech).toBeUndefined();
|
|
223
|
-
// Others remain
|
|
224
|
-
expect(result.one).toBe('1');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('nested exclusion via stabilization rules', () => {
|
|
228
|
-
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
|
|
229
|
-
const result = filterThumbmarkData(testData, {
|
|
230
|
-
...defaultOptions,
|
|
231
|
-
stabilize: ['private'],
|
|
232
|
-
});
|
|
233
|
-
// audio.sampleHash excluded for Brave, but audio.other should remain
|
|
234
|
-
expect(result.audio).toBeDefined();
|
|
235
|
-
expect((result.audio as componentInterface).other).toBe('jkl');
|
|
236
|
-
expect((result.audio as componentInterface).sampleHash).toBeUndefined();
|
|
237
|
-
});
|
|
238
|
-
});
|