@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,273 +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
|
-
test('non-array string exclude does not throw and is ignored', () => {
|
|
166
|
-
const result = getExcludeList({ ...defaultOptions, exclude: 'one' as any, stabilize: [] });
|
|
167
|
-
expect(Array.isArray(result)).toBe(true);
|
|
168
|
-
expect(result).not.toContain('one');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('non-array object exclude does not throw and is ignored', () => {
|
|
172
|
-
const result = getExcludeList({ ...defaultOptions, exclude: {} as any, stabilize: [] });
|
|
173
|
-
expect(Array.isArray(result)).toBe(true);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ── filterThumbmarkData ─────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
describe('filterThumbmarkData', () => {
|
|
180
|
-
beforeEach(() => {
|
|
181
|
-
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('returns full object with no options', () => {
|
|
185
|
-
const result = filterThumbmarkData(testData);
|
|
186
|
-
expect(result).toEqual(testData);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('returns full object with empty exclude', () => {
|
|
190
|
-
const result = filterThumbmarkData(testData, { ...defaultOptions, exclude: [], stabilize: [] });
|
|
191
|
-
expect(result).toEqual(testData);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('include overrides exclude for the same key', () => {
|
|
195
|
-
const result = filterThumbmarkData(testData, {
|
|
196
|
-
...defaultOptions,
|
|
197
|
-
exclude: ['one'],
|
|
198
|
-
include: ['one'],
|
|
199
|
-
stabilize: [],
|
|
200
|
-
});
|
|
201
|
-
expect(result.one).toBe('1');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('include overrides exclude at nested level', () => {
|
|
205
|
-
const result = filterThumbmarkData(testData, {
|
|
206
|
-
...defaultOptions,
|
|
207
|
-
exclude: ['three'],
|
|
208
|
-
include: ['three.a'],
|
|
209
|
-
stabilize: [],
|
|
210
|
-
});
|
|
211
|
-
expect(result.three).toEqual({ a: true });
|
|
212
|
-
expect((result.three as componentInterface).b).toBeUndefined();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('excluding a parent removes all children', () => {
|
|
216
|
-
const result = filterThumbmarkData(testData, {
|
|
217
|
-
...defaultOptions,
|
|
218
|
-
exclude: ['three'],
|
|
219
|
-
stabilize: [],
|
|
220
|
-
});
|
|
221
|
-
expect(result.three).toBeUndefined();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('stabilization rules integrate with filterThumbmarkData', () => {
|
|
225
|
-
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
|
|
226
|
-
const result = filterThumbmarkData(testData, {
|
|
227
|
-
...defaultOptions,
|
|
228
|
-
stabilize: ['private'],
|
|
229
|
-
});
|
|
230
|
-
// Brave+private excludes canvas
|
|
231
|
-
expect(result.canvas).toBeUndefined();
|
|
232
|
-
// Brave+'always' excludes speech
|
|
233
|
-
expect(result.speech).toBeUndefined();
|
|
234
|
-
// Others remain
|
|
235
|
-
expect(result.one).toBe('1');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('nested exclusion via stabilization rules', () => {
|
|
239
|
-
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
|
|
240
|
-
const result = filterThumbmarkData(testData, {
|
|
241
|
-
...defaultOptions,
|
|
242
|
-
stabilize: ['private'],
|
|
243
|
-
});
|
|
244
|
-
// audio.sampleHash excluded for Brave, but audio.other should remain
|
|
245
|
-
expect(result.audio).toBeDefined();
|
|
246
|
-
expect((result.audio as componentInterface).other).toBe('jkl');
|
|
247
|
-
expect((result.audio as componentInterface).sampleHash).toBeUndefined();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('non-array string include does not throw and does not rescue excluded keys', () => {
|
|
251
|
-
const result = filterThumbmarkData(testData, {
|
|
252
|
-
...defaultOptions,
|
|
253
|
-
exclude: ['one'],
|
|
254
|
-
include: 'one' as any,
|
|
255
|
-
stabilize: [],
|
|
256
|
-
});
|
|
257
|
-
// Bad include is treated as empty, so 'one' stays excluded
|
|
258
|
-
expect(result.one).toBeUndefined();
|
|
259
|
-
expect(result.two).toBe(2);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test('null include does not throw and behaves as empty include', () => {
|
|
263
|
-
const result = filterThumbmarkData(testData, {
|
|
264
|
-
...defaultOptions,
|
|
265
|
-
exclude: ['two'],
|
|
266
|
-
include: null as any,
|
|
267
|
-
stabilize: [],
|
|
268
|
-
});
|
|
269
|
-
// null include treated as empty, so 'two' stays excluded
|
|
270
|
-
expect(result.two).toBeUndefined();
|
|
271
|
-
expect(result.one).toBe('1');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { componentInterface } from '../factory'
|
|
2
|
-
import { filterThumbmarkData } from './filterComponents'
|
|
3
|
-
import { resolveClientComponents, getThumbmark } from '.';
|
|
4
|
-
import { defaultOptions } from '../options';
|
|
5
|
-
|
|
6
|
-
const test_components: componentInterface = {
|
|
7
|
-
'one': '1',
|
|
8
|
-
'two': 2,
|
|
9
|
-
'three': {'a': true, 'b': false}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('component filtering tests', () => {
|
|
13
|
-
test("excluding top level works", () => {
|
|
14
|
-
expect(filterThumbmarkData(test_components,
|
|
15
|
-
{ ...defaultOptions, ...{ exclude: ['one']} }
|
|
16
|
-
)).toMatchObject({
|
|
17
|
-
'two': 2, 'three': {'a': true, 'b': false}
|
|
18
|
-
})
|
|
19
|
-
});
|
|
20
|
-
test("including top level works", () => {
|
|
21
|
-
expect(filterThumbmarkData(test_components, { ...defaultOptions, ...{ include: ['one', 'two']} })).toMatchObject({
|
|
22
|
-
'one': '1', 'two': 2
|
|
23
|
-
})
|
|
24
|
-
});
|
|
25
|
-
test("excluding low-level works", () => {
|
|
26
|
-
expect(filterThumbmarkData(test_components,
|
|
27
|
-
{ ...defaultOptions, ...{ exclude: ['two', 'three.a']} }
|
|
28
|
-
)).toMatchObject({
|
|
29
|
-
'one': '1',
|
|
30
|
-
'three': {'b': false}
|
|
31
|
-
})
|
|
32
|
-
});
|
|
33
|
-
test("including low-level works", () => {
|
|
34
|
-
expect(filterThumbmarkData(test_components,
|
|
35
|
-
{ ...defaultOptions, ...{ include: ['one', 'three.b']} })).toMatchObject({
|
|
36
|
-
'one': '1',
|
|
37
|
-
'three': {'b': false}
|
|
38
|
-
})
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('resolveClientComponents runtime stability', () => {
|
|
43
|
-
test('continues resolving when one component promise rejects', async () => {
|
|
44
|
-
const components = {
|
|
45
|
-
stable: async () => ({ value: 'ok' } as componentInterface),
|
|
46
|
-
unstable: async () => Promise.reject(new Error('component failed')),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const { resolvedComponents, elapsed, errors } = await resolveClientComponents(components, {
|
|
50
|
-
...defaultOptions,
|
|
51
|
-
stabilize: [],
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(resolvedComponents.stable).toEqual({ value: 'ok' });
|
|
55
|
-
expect(resolvedComponents.unstable).toEqual({ timeout: 'true' });
|
|
56
|
-
expect(elapsed.stable).toBeGreaterThanOrEqual(0);
|
|
57
|
-
expect(elapsed.unstable).toBeGreaterThanOrEqual(0);
|
|
58
|
-
expect(errors).toEqual([
|
|
59
|
-
{ type: 'component_error', message: 'component failed', component: 'unstable' }
|
|
60
|
-
]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('reports component timeout in errors', async () => {
|
|
64
|
-
const components = {
|
|
65
|
-
fast: async () => ({ value: 'ok' } as componentInterface),
|
|
66
|
-
slow: () => new Promise<componentInterface>(() => {}),
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const { resolvedComponents, errors } = await resolveClientComponents(components, {
|
|
70
|
-
...defaultOptions,
|
|
71
|
-
timeout: 10,
|
|
72
|
-
stabilize: [],
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(resolvedComponents.fast).toEqual({ value: 'ok' });
|
|
76
|
-
expect(errors).toEqual([
|
|
77
|
-
{ type: 'component_timeout', message: "Component 'slow' timed out", component: 'slow' }
|
|
78
|
-
]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('returns empty errors when all components succeed', async () => {
|
|
82
|
-
const components = {
|
|
83
|
-
a: async () => ({ value: '1' } as componentInterface),
|
|
84
|
-
b: async () => ({ value: '2' } as componentInterface),
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const { errors } = await resolveClientComponents(components, {
|
|
88
|
-
...defaultOptions,
|
|
89
|
-
stabilize: [],
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
expect(errors).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('getThumbmark error reporting', () => {
|
|
97
|
-
test('returns api_unauthorized error for 403 response', async () => {
|
|
98
|
-
const originalFetch = global.fetch;
|
|
99
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
100
|
-
ok: false,
|
|
101
|
-
status: 403,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const result = await getThumbmark({
|
|
105
|
-
...defaultOptions,
|
|
106
|
-
api_key: 'fake-invalid-key',
|
|
107
|
-
cache_api_call: false,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(result.error).toEqual([
|
|
111
|
-
{ type: 'api_unauthorized', message: 'Invalid API key or quota exceeded' }
|
|
112
|
-
]);
|
|
113
|
-
expect(result.thumbmark).toBe('');
|
|
114
|
-
|
|
115
|
-
global.fetch = originalFetch;
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('returns api_error for 5xx response', async () => {
|
|
119
|
-
const originalFetch = global.fetch;
|
|
120
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
121
|
-
ok: false,
|
|
122
|
-
status: 500,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const result = await getThumbmark({
|
|
126
|
-
...defaultOptions,
|
|
127
|
-
api_key: 'some-key',
|
|
128
|
-
cache_api_call: false,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
expect(result.error).toEqual(
|
|
132
|
-
expect.arrayContaining([
|
|
133
|
-
expect.objectContaining({ type: 'api_error', message: 'HTTP error! status: 500' })
|
|
134
|
-
])
|
|
135
|
-
);
|
|
136
|
-
// Should still return a fingerprint (graceful degradation)
|
|
137
|
-
expect(result.thumbmark).toBeDefined();
|
|
138
|
-
expect(result.thumbmark.length).toBeGreaterThan(0);
|
|
139
|
-
|
|
140
|
-
global.fetch = originalFetch;
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { getApiPromise, apiResponse } from './api';
|
|
2
|
-
import { OptionsAfterDefaults } from '../options';
|
|
3
|
-
import { componentInterface } from '../factory';
|
|
4
|
-
|
|
5
|
-
// Mock fetch globally
|
|
6
|
-
global.fetch = jest.fn();
|
|
7
|
-
|
|
8
|
-
describe('Metadata Support', () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
jest.clearAllMocks();
|
|
11
|
-
(global.fetch as jest.Mock).mockResolvedValue({
|
|
12
|
-
ok: true,
|
|
13
|
-
json: async () => ({
|
|
14
|
-
thumbmark: 'test-thumbmark',
|
|
15
|
-
visitorId: 'test-visitor',
|
|
16
|
-
requestId: 'test-request',
|
|
17
|
-
metadata: 'test-metadata',
|
|
18
|
-
}),
|
|
19
|
-
});
|
|
20
|
-
// Spy on console.error
|
|
21
|
-
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
jest.restoreAllMocks();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should include static metadata string in request body', async () => {
|
|
29
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
30
|
-
api_key: 'test-key',
|
|
31
|
-
metadata: 'event-123',
|
|
32
|
-
cache_api_call: false,
|
|
33
|
-
};
|
|
34
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
35
|
-
|
|
36
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
37
|
-
|
|
38
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
39
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
40
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
41
|
-
expect(requestBody.metadata).toBe('event-123');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should evaluate metadata function and include result in request body', async () => {
|
|
45
|
-
let counter = 0;
|
|
46
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
47
|
-
api_key: 'test-key',
|
|
48
|
-
metadata: () => `call-${++counter}`,
|
|
49
|
-
cache_api_call: false,
|
|
50
|
-
};
|
|
51
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
52
|
-
|
|
53
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
54
|
-
|
|
55
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
56
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
57
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
58
|
-
expect(requestBody.metadata).toBe('call-1');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should call metadata function each time get is invoked', async () => {
|
|
62
|
-
let counter = 0;
|
|
63
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
64
|
-
api_key: 'test-key',
|
|
65
|
-
metadata: () => `call-${++counter}`,
|
|
66
|
-
cache_api_call: false,
|
|
67
|
-
};
|
|
68
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
69
|
-
|
|
70
|
-
// First call
|
|
71
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
72
|
-
const firstCallArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
73
|
-
const firstRequestBody = JSON.parse(firstCallArgs[1].body);
|
|
74
|
-
expect(firstRequestBody.metadata).toBe('call-1');
|
|
75
|
-
|
|
76
|
-
// Second call
|
|
77
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
78
|
-
const secondCallArgs = (global.fetch as jest.Mock).mock.calls[1];
|
|
79
|
-
const secondRequestBody = JSON.parse(secondCallArgs[1].body);
|
|
80
|
-
expect(secondRequestBody.metadata).toBe('call-2');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should omit metadata from request body when not provided', async () => {
|
|
84
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
85
|
-
api_key: 'test-key',
|
|
86
|
-
cache_api_call: false,
|
|
87
|
-
};
|
|
88
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
89
|
-
|
|
90
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
91
|
-
|
|
92
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
93
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
94
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
95
|
-
expect(requestBody.metadata).toBeUndefined();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should echo metadata back in API response', async () => {
|
|
99
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
100
|
-
api_key: 'test-key',
|
|
101
|
-
metadata: '{"eventId": "12345"}',
|
|
102
|
-
cache_api_call: false,
|
|
103
|
-
};
|
|
104
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
105
|
-
|
|
106
|
-
const response = await getApiPromise(options as OptionsAfterDefaults, components);
|
|
107
|
-
|
|
108
|
-
expect(response).toBeDefined();
|
|
109
|
-
expect(response?.metadata).toBe('test-metadata');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should handle various string metadata types', async () => {
|
|
113
|
-
const testCases = [
|
|
114
|
-
'simple-string',
|
|
115
|
-
'{"json": "object"}',
|
|
116
|
-
'encrypted:abc123def456',
|
|
117
|
-
'multi\nline\nstring',
|
|
118
|
-
'🎉 unicode 中文',
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
for (const metadata of testCases) {
|
|
122
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
123
|
-
api_key: 'test-key',
|
|
124
|
-
metadata,
|
|
125
|
-
cache_api_call: false,
|
|
126
|
-
};
|
|
127
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
128
|
-
|
|
129
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
130
|
-
|
|
131
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[
|
|
132
|
-
(global.fetch as jest.Mock).mock.calls.length - 1
|
|
133
|
-
];
|
|
134
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
135
|
-
expect(requestBody.metadata).toBe(metadata);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should handle empty string metadata', async () => {
|
|
140
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
141
|
-
api_key: 'test-key',
|
|
142
|
-
metadata: '',
|
|
143
|
-
cache_api_call: false,
|
|
144
|
-
};
|
|
145
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
146
|
-
|
|
147
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
148
|
-
|
|
149
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
150
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
151
|
-
// Empty string is falsy, so it won't be included
|
|
152
|
-
expect(requestBody.metadata).toBeUndefined();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should handle metadata function returning empty string', async () => {
|
|
156
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
157
|
-
api_key: 'test-key',
|
|
158
|
-
metadata: () => '',
|
|
159
|
-
cache_api_call: false,
|
|
160
|
-
};
|
|
161
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
162
|
-
|
|
163
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
164
|
-
|
|
165
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
166
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
167
|
-
// Empty string is falsy, so it won't be included
|
|
168
|
-
expect(requestBody.metadata).toBeUndefined();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should log error and omit metadata if length exceeds 1000 characters', async () => {
|
|
172
|
-
const longMetadata = 'a'.repeat(1001);
|
|
173
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
174
|
-
api_key: 'test-key',
|
|
175
|
-
metadata: longMetadata,
|
|
176
|
-
cache_api_call: false,
|
|
177
|
-
};
|
|
178
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
179
|
-
|
|
180
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
181
|
-
|
|
182
|
-
// Check that fetch was called
|
|
183
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
184
|
-
|
|
185
|
-
// precise console error check
|
|
186
|
-
expect(console.error).toHaveBeenCalledWith('ThumbmarkJS: Metadata exceeds 1000 characters. Skipping metadata.');
|
|
187
|
-
|
|
188
|
-
// Check that metadata was omitted
|
|
189
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
190
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
191
|
-
expect(requestBody.metadata).toBeUndefined();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should accept metadata exactly 1000 characters check', async () => {
|
|
195
|
-
const validMetadata = 'a'.repeat(1000);
|
|
196
|
-
const options: Partial<OptionsAfterDefaults> = {
|
|
197
|
-
api_key: 'test-key',
|
|
198
|
-
metadata: validMetadata,
|
|
199
|
-
cache_api_call: false,
|
|
200
|
-
};
|
|
201
|
-
const components: componentInterface = { system: { browser: 'chrome' } };
|
|
202
|
-
|
|
203
|
-
await getApiPromise(options as OptionsAfterDefaults, components);
|
|
204
|
-
|
|
205
|
-
expect(console.error).not.toHaveBeenCalledWith('ThumbmarkJS: Metadata exceeds 1000 characters. Skipping metadata.');
|
|
206
|
-
|
|
207
|
-
const callArgs = (global.fetch as jest.Mock).mock.calls[0];
|
|
208
|
-
const requestBody = JSON.parse(callArgs[1].body);
|
|
209
|
-
expect(requestBody.metadata).toBe(validMetadata);
|
|
210
|
-
});
|
|
211
|
-
});
|
package/src/options.test.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_STORAGE_PREFIX, defaultOptions } from "./options";
|
|
2
|
-
|
|
3
|
-
describe('property_name_factory', () => {
|
|
4
|
-
test('it should default to the default value', () => {
|
|
5
|
-
const name = 'mykey';
|
|
6
|
-
expect(
|
|
7
|
-
defaultOptions.property_name_factory(name)
|
|
8
|
-
).toEqual(`${DEFAULT_STORAGE_PREFIX}_${name}`);
|
|
9
|
-
})
|
|
10
|
-
})
|