@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,87 +0,0 @@
1
- import { Thumbmark } from './thumbmark';
2
- import { customComponents } from './factory';
3
- import { getThumbmark, includeComponent as includeGlobalComponent } from './index';
4
-
5
- describe('Thumbmark custom components', () => {
6
- beforeEach(() => {
7
- for (const key of Object.keys(customComponents)) {
8
- delete customComponents[key];
9
- }
10
- });
11
-
12
- test('custom components stay scoped to the instance that registered them', async () => {
13
- const tm1 = new Thumbmark({ logging: false });
14
- const tm2 = new Thumbmark({ logging: false });
15
-
16
- tm1.includeComponent('instanceOnly', async () => ({
17
- value: 'tm1'
18
- }));
19
-
20
- const tm1Result = await tm1.get({ include: ['instanceOnly'] });
21
- const tm2Result = await tm2.get({ include: ['instanceOnly'] });
22
-
23
- expect(tm1Result.components.instanceOnly).toEqual({ value: 'tm1' });
24
- expect(tm2Result.components.instanceOnly).toBeUndefined();
25
- });
26
-
27
- test('deprecated global includeComponent still registers components for getThumbmark', async () => {
28
- includeGlobalComponent('legacyGlobal', async () => ({
29
- value: 'global'
30
- }));
31
-
32
- const result = await getThumbmark({
33
- logging: false,
34
- include: ['legacyGlobal']
35
- });
36
-
37
- expect(result.components.legacyGlobal).toEqual({ value: 'global' });
38
- });
39
-
40
- test('getThumbmark accepts an optional custom component registry', async () => {
41
- const result = await getThumbmark({
42
- logging: false,
43
- include: ['directCustom']
44
- }, {
45
- directCustom: async () => ({
46
- value: 'direct'
47
- })
48
- });
49
-
50
- expect(result.components.directCustom).toEqual({ value: 'direct' });
51
- });
52
-
53
- test('deprecated global includeComponent still registers components for Thumbmark.get()', async () => {
54
- includeGlobalComponent('legacyGlobal', async () => ({
55
- value: 'global'
56
- }));
57
-
58
- const tm = new Thumbmark({ logging: false });
59
- const result = await tm.get({
60
- include: ['legacyGlobal']
61
- });
62
-
63
- expect(result.components.legacyGlobal).toEqual({ value: 'global' });
64
- });
65
-
66
- test('instance custom components override deprecated global ones for the same key', async () => {
67
- includeGlobalComponent('sharedKey', async () => ({
68
- value: 'global'
69
- }));
70
-
71
- const tm = new Thumbmark({ logging: false });
72
- tm.includeComponent('sharedKey', async () => ({
73
- value: 'instance'
74
- }));
75
-
76
- const globalResult = await getThumbmark({
77
- logging: false,
78
- include: ['sharedKey']
79
- });
80
- const instanceResult = await tm.get({
81
- include: ['sharedKey']
82
- });
83
-
84
- expect(globalResult.components.sharedKey).toEqual({ value: 'global' });
85
- expect(instanceResult.components.sharedKey).toEqual({ value: 'instance' });
86
- });
87
- });
@@ -1,59 +0,0 @@
1
- /**
2
- * Tests for Thumbmark class behavior in Node/Jest environment
3
- *
4
- * These tests verify that Thumbmark works gracefully when browser APIs
5
- * are not available (e.g., in Jest/jsdom or server-side environments).
6
- */
7
-
8
- import { Thumbmark } from './thumbmark';
9
- import { jest } from '@jest/globals';
10
-
11
- // Polyfill for sessionStorage in Node environment
12
- if (typeof sessionStorage === 'undefined') {
13
- (global as any).sessionStorage = {
14
- getItem: jest.fn(),
15
- setItem: jest.fn(),
16
- removeItem: jest.fn(),
17
- clear: jest.fn(),
18
- length: 0,
19
- key: jest.fn(),
20
- };
21
- }
22
-
23
-
24
- describe('Thumbmark in Node/Jest environment', () => {
25
- test('Thumbmark.get() should not throw in non-browser environment', async () => {
26
- const tm = new Thumbmark();
27
-
28
- // This should not throw, even if browser APIs are unavailable
29
- const result = await tm.get();
30
-
31
- // Should return a valid result with structure
32
- expect(result).toBeDefined();
33
- expect(result.thumbmark).toBeDefined();
34
- expect(typeof result.thumbmark).toBe('string');
35
- });
36
-
37
- test('Thumbmark.get() returns valid structure even with missing browser APIs', async () => {
38
- const tm = new Thumbmark();
39
-
40
- const result = await tm.get();
41
-
42
- // Verify the response structure
43
- expect(result).toHaveProperty('thumbmark');
44
- expect(result).toHaveProperty('components');
45
- expect(result).toHaveProperty('version');
46
- expect(result).toHaveProperty('info');
47
-
48
- // Components should be an object (possibly with fewer values than in real browser)
49
- expect(typeof result.components).toBe('object');
50
- });
51
-
52
- test('Thumbmark.getVersion() works in any environment', () => {
53
- const tm = new Thumbmark();
54
- const version = tm.getVersion();
55
-
56
- expect(version).toBeDefined();
57
- expect(typeof version).toBe('string');
58
- });
59
- });
@@ -1,95 +0,0 @@
1
- import { Cache, getCache, setCache, getApiResponseExpiry, CACHE_KEY } from "./cache";
2
- import { DEFAULT_STORAGE_PREFIX, defaultOptions, MAXIMUM_CACHE_LIFETIME } from "../options";
3
-
4
- const _options = {
5
- property_name_factory: (name: string) => {
6
- return `${name}-mypostfix`;
7
- },
8
- cache_lifetime_in_ms: 0,
9
- };
10
-
11
- const values: Cache = {
12
- apiResponseExpiry: 100,
13
- apiResponse: {
14
- version: '1.2.3',
15
- },
16
- };
17
-
18
- const cacheProperty = defaultOptions.property_name_factory(CACHE_KEY);
19
- const customCacheProperty = _options.property_name_factory(CACHE_KEY);
20
-
21
- describe('getCache', () => {
22
- beforeEach(() => {
23
- localStorage.clear();
24
- });
25
-
26
- test('it should return all values from cache', () => {
27
- localStorage.setItem(cacheProperty, JSON.stringify(values));
28
- const fromStorage = getCache(defaultOptions);
29
-
30
- expect(fromStorage).toEqual(values);
31
- });
32
-
33
- test('it should return an empty object in case of nothing cached', () => {
34
- expect(getCache(defaultOptions)).toEqual({});
35
- });
36
-
37
- test('it should return an empty object in case non-json content', () => {
38
- localStorage.setItem(cacheProperty, 'abc123');
39
- expect(getCache(defaultOptions)).toEqual({});
40
- });
41
-
42
- test('it should return from the correct property', () => {
43
- localStorage.setItem(cacheProperty, JSON.stringify(values));
44
-
45
- expect(getCache(_options)).toEqual({});
46
- expect(getCache(defaultOptions)).toEqual(values);
47
- });
48
- })
49
-
50
- describe('setCache', () => {
51
- beforeEach(() => {
52
- localStorage.clear();
53
- });
54
-
55
- test('it should write given values', () => {
56
- setCache(defaultOptions, values);
57
- expect(JSON.parse(localStorage.getItem(cacheProperty)!))
58
- .toEqual(values);
59
- });
60
-
61
- test('it should not touch values that are not provided', () => {
62
- setCache(_options, values);
63
- setCache(_options, {
64
- apiResponseExpiry: 200
65
- });
66
- const fromStorage = JSON.parse(localStorage.getItem(customCacheProperty)!) as Cache;
67
- expect(fromStorage.apiResponseExpiry).toEqual(200);
68
- expect(fromStorage.apiResponse!.version).toEqual(values.apiResponse!.version);
69
- })
70
- })
71
-
72
- describe('getApiResponseExpiry', () => {
73
- const systemTime = 1000;
74
- beforeAll(() => {
75
- jest.useFakeTimers().setSystemTime(systemTime);
76
- localStorage.clear();
77
- });
78
-
79
- test('it should return a value based on options', () => {
80
- expect(getApiResponseExpiry({
81
- cache_lifetime_in_ms: 100,
82
- })).toBe(systemTime + 100);
83
- });
84
-
85
- test('it should not go over maximum cache lifetime', () => {
86
- expect(getApiResponseExpiry({
87
- cache_lifetime_in_ms: MAXIMUM_CACHE_LIFETIME + 200,
88
- })).toBe(systemTime + MAXIMUM_CACHE_LIFETIME);
89
- })
90
-
91
- afterAll(() => {
92
- jest.useRealTimers()
93
- })
94
- })
95
-
@@ -1,86 +0,0 @@
1
- import { raceAllPerformance, raceAll } from './raceAll';
2
-
3
- describe('raceAll', () => {
4
- test('returns resolved values when promises fulfill', async () => {
5
- const results = await raceAll(
6
- [Promise.resolve('ok')],
7
- 50,
8
- 'timeout'
9
- );
10
-
11
- expect(results).toHaveLength(1);
12
- expect(results[0]).toBe('ok');
13
- });
14
-
15
- test('returns timeout fallback when promise does not settle in time', async () => {
16
- const neverResolves = new Promise<string>(() => { });
17
- const results = await raceAll(
18
- [neverResolves],
19
- 10,
20
- 'timeout'
21
- );
22
-
23
- expect(results).toHaveLength(1);
24
- expect(results[0]).toBe('timeout');
25
- });
26
-
27
- test('does not reject the whole batch when one promise rejects', async () => {
28
- const rejects = Promise.reject(new Error('component failed'));
29
- const resolves = Promise.resolve('ok');
30
-
31
- const results = await raceAll(
32
- [rejects, resolves],
33
- 50,
34
- 'timeout'
35
- );
36
-
37
- expect(results).toHaveLength(2);
38
- expect(results[0]).toBe('timeout');
39
- expect(results[1]).toBe('ok');
40
- });
41
- });
42
-
43
- describe('raceAllPerformance', () => {
44
- test('returns resolved values when promises fulfill', async () => {
45
- const results = await raceAllPerformance(
46
- [Promise.resolve('ok')],
47
- 50,
48
- 'timeout'
49
- );
50
-
51
- expect(results).toHaveLength(1);
52
- expect(results[0].value).toBe('ok');
53
- expect(typeof results[0].elapsed).toBe('number');
54
- expect(results[0].error).toBeUndefined();
55
- });
56
-
57
- test('returns timeout fallback when promise does not settle in time', async () => {
58
- const neverResolves = new Promise<string>(() => { });
59
- const results = await raceAllPerformance(
60
- [neverResolves],
61
- 10,
62
- 'timeout'
63
- );
64
-
65
- expect(results).toHaveLength(1);
66
- expect(results[0].value).toBe('timeout');
67
- expect(results[0].error).toBe('timeout');
68
- });
69
-
70
- test('does not reject the whole batch when one promise rejects', async () => {
71
- const rejects = Promise.reject(new Error('component failed'));
72
- const resolves = Promise.resolve('ok');
73
-
74
- const results = await raceAllPerformance(
75
- [rejects, resolves],
76
- 50,
77
- 'timeout'
78
- );
79
-
80
- expect(results).toHaveLength(2);
81
- expect(results[0].value).toBe('timeout');
82
- expect(results[0].error).toBe('component failed');
83
- expect(results[1].value).toBe('ok');
84
- expect(results[1].error).toBeUndefined();
85
- });
86
- });
@@ -1,335 +0,0 @@
1
- import { stableStringify } from './stableStringify';
2
-
3
- describe('stableStringify', () => {
4
- describe('basic functionality', () => {
5
- test('sorts object keys alphabetically', () => {
6
- const obj = { z: 3, a: 1, m: 2 };
7
- const result = stableStringify(obj);
8
- expect(result).toBe('{"a":1,"m":2,"z":3}');
9
- });
10
-
11
- test('produces valid JSON', () => {
12
- const obj = { b: 2, a: 1, c: 3 };
13
- const result = stableStringify(obj);
14
- expect(() => JSON.parse(result)).not.toThrow();
15
- expect(JSON.parse(result)).toEqual({ a: 1, b: 2, c: 3 });
16
- });
17
-
18
- test('produces consistent output for same input', () => {
19
- const obj = { z: 3, a: 1, m: 2 };
20
- const result1 = stableStringify(obj);
21
- const result2 = stableStringify(obj);
22
- expect(result1).toBe(result2);
23
- });
24
-
25
- test('produces same output regardless of key insertion order', () => {
26
- const obj1 = { a: 1, b: 2, c: 3 };
27
- const obj2 = { c: 3, a: 1, b: 2 };
28
- const obj3 = { b: 2, c: 3, a: 1 };
29
-
30
- const result1 = stableStringify(obj1);
31
- const result2 = stableStringify(obj2);
32
- const result3 = stableStringify(obj3);
33
-
34
- expect(result1).toBe(result2);
35
- expect(result2).toBe(result3);
36
- });
37
- });
38
-
39
- describe('nested objects', () => {
40
- test('sorts keys in nested objects', () => {
41
- const obj = {
42
- z: { y: 2, x: 1 },
43
- a: { c: 4, b: 3 }
44
- };
45
- const result = stableStringify(obj);
46
- expect(result).toBe('{"a":{"b":3,"c":4},"z":{"x":1,"y":2}}');
47
- });
48
-
49
- test('handles deeply nested objects', () => {
50
- const obj = {
51
- level1: {
52
- z: 'last',
53
- a: {
54
- nested: {
55
- z: 3,
56
- a: 1,
57
- m: 2
58
- }
59
- }
60
- }
61
- };
62
- const result = stableStringify(obj);
63
- const parsed = JSON.parse(result);
64
- expect(parsed).toEqual({
65
- level1: {
66
- a: {
67
- nested: {
68
- a: 1,
69
- m: 2,
70
- z: 3
71
- }
72
- },
73
- z: 'last'
74
- }
75
- });
76
- });
77
- });
78
-
79
- describe('arrays', () => {
80
- test('preserves array order', () => {
81
- const arr = [3, 1, 2];
82
- const result = stableStringify(arr);
83
- expect(result).toBe('[3,1,2]');
84
- });
85
-
86
- test('handles arrays with objects', () => {
87
- const arr = [
88
- { z: 2, a: 1 },
89
- { b: 4, a: 3 }
90
- ];
91
- const result = stableStringify(arr);
92
- expect(result).toBe('[{"a":1,"z":2},{"a":3,"b":4}]');
93
- });
94
-
95
- test('handles nested arrays', () => {
96
- const arr = [[3, 2, 1], [6, 5, 4]];
97
- const result = stableStringify(arr);
98
- expect(result).toBe('[[3,2,1],[6,5,4]]');
99
- });
100
-
101
- test('handles mixed arrays', () => {
102
- const arr = [1, 'string', { z: 2, a: 1 }, [3, 4], null, true];
103
- const result = stableStringify(arr);
104
- expect(result).toBe('[1,"string",{"a":1,"z":2},[3,4],null,true]');
105
- });
106
- });
107
-
108
- describe('primitive types', () => {
109
- test('handles strings', () => {
110
- expect(stableStringify('hello')).toBe('"hello"');
111
- });
112
-
113
- test('handles numbers', () => {
114
- expect(stableStringify(42)).toBe('42');
115
- expect(stableStringify(0)).toBe('0');
116
- expect(stableStringify(-42)).toBe('-42');
117
- expect(stableStringify(3.14)).toBe('3.14');
118
- });
119
-
120
- test('handles booleans', () => {
121
- expect(stableStringify(true)).toBe('true');
122
- expect(stableStringify(false)).toBe('false');
123
- });
124
-
125
- test('handles null', () => {
126
- expect(stableStringify(null)).toBe('null');
127
- });
128
-
129
- test('handles undefined', () => {
130
- expect(stableStringify(undefined)).toBe('');
131
- });
132
-
133
- test('handles undefined in objects', () => {
134
- const obj = { a: 1, b: undefined, c: 3 };
135
- const result = stableStringify(obj);
136
- expect(result).toBe('{"a":1,"c":3}');
137
- });
138
-
139
- test('handles undefined in arrays', () => {
140
- const arr = [1, undefined, 3];
141
- const result = stableStringify(arr);
142
- expect(result).toBe('[1,null,3]');
143
- });
144
- });
145
-
146
- describe('special number values', () => {
147
- test('handles Infinity as null', () => {
148
- expect(stableStringify(Infinity)).toBe('null');
149
- });
150
-
151
- test('handles -Infinity as null', () => {
152
- expect(stableStringify(-Infinity)).toBe('null');
153
- });
154
-
155
- test('handles NaN as null', () => {
156
- expect(stableStringify(NaN)).toBe('null');
157
- });
158
-
159
- test('handles special numbers in objects', () => {
160
- const obj = { a: Infinity, b: NaN, c: -Infinity };
161
- const result = stableStringify(obj);
162
- expect(result).toBe('{"a":null,"b":null,"c":null}');
163
- });
164
- });
165
-
166
- describe('circular references', () => {
167
- test('throws TypeError on circular reference', () => {
168
- const obj: any = { a: 1 };
169
- obj.self = obj;
170
-
171
- expect(() => stableStringify(obj)).toThrow(TypeError);
172
- expect(() => stableStringify(obj)).toThrow('Converting circular structure to JSON');
173
- });
174
-
175
- test('throws on nested circular reference', () => {
176
- const obj: any = { a: { b: {} } };
177
- obj.a.b.circular = obj;
178
-
179
- expect(() => stableStringify(obj)).toThrow(TypeError);
180
- });
181
-
182
- test('throws on array circular reference', () => {
183
- const arr: any = [1, 2, 3];
184
- arr.push(arr);
185
-
186
- // Note: Array circular references cause stack overflow (RangeError)
187
- // rather than being caught by the circular reference check
188
- expect(() => stableStringify(arr)).toThrow(RangeError);
189
- });
190
- });
191
-
192
- describe('toJSON method', () => {
193
- test('calls toJSON method if present', () => {
194
- const obj = {
195
- value: 42,
196
- toJSON() {
197
- return { transformed: this.value * 2 };
198
- }
199
- };
200
- const result = stableStringify(obj);
201
- expect(result).toBe('{"transformed":84}');
202
- });
203
-
204
- test('handles Date objects via toJSON', () => {
205
- const date = new Date('2023-01-01T00:00:00.000Z');
206
- const result = stableStringify(date);
207
- expect(result).toBe('"2023-01-01T00:00:00.000Z"');
208
- });
209
-
210
- test('handles nested objects with toJSON', () => {
211
- const obj = {
212
- z: 'last',
213
- a: {
214
- toJSON() {
215
- return { custom: 'value' };
216
- }
217
- }
218
- };
219
- const result = stableStringify(obj);
220
- expect(result).toBe('{"a":{"custom":"value"},"z":"last"}');
221
- });
222
- });
223
-
224
- describe('complex scenarios', () => {
225
- test('handles empty object', () => {
226
- expect(stableStringify({})).toBe('{}');
227
- });
228
-
229
- test('handles empty array', () => {
230
- expect(stableStringify([])).toBe('[]');
231
- });
232
-
233
- test('handles complex nested structure', () => {
234
- const complex = {
235
- users: [
236
- { name: 'Alice', age: 30, active: true },
237
- { name: 'Bob', age: 25, active: false }
238
- ],
239
- metadata: {
240
- version: 1,
241
- timestamp: 1234567890,
242
- config: {
243
- enabled: true,
244
- options: ['a', 'b', 'c']
245
- }
246
- },
247
- count: 42
248
- };
249
-
250
- const result = stableStringify(complex);
251
- const parsed = JSON.parse(result);
252
-
253
- // Verify it's valid JSON
254
- expect(parsed).toBeDefined();
255
-
256
- // Verify structure is preserved
257
- expect(parsed.users).toHaveLength(2);
258
- expect(parsed.metadata.config.options).toEqual(['a', 'b', 'c']);
259
- expect(parsed.count).toBe(42);
260
- });
261
-
262
- test('handles objects with special characters in keys', () => {
263
- const obj = {
264
- 'key with spaces': 1,
265
- 'key-with-dashes': 2,
266
- 'key_with_underscores': 3,
267
- 'key.with.dots': 4
268
- };
269
- const result = stableStringify(obj);
270
- const parsed = JSON.parse(result);
271
- expect(parsed).toEqual(obj);
272
- });
273
-
274
- test('handles objects with numeric string keys', () => {
275
- const obj = { '2': 'two', '1': 'one', '10': 'ten' };
276
- const result = stableStringify(obj);
277
- // Keys should be sorted as strings: "1", "10", "2"
278
- expect(result).toBe('{"1":"one","10":"ten","2":"two"}');
279
- });
280
- });
281
-
282
- describe('JSON validity', () => {
283
- test('output is always valid JSON for valid inputs', () => {
284
- const testCases = [
285
- { a: 1, b: 2 },
286
- [1, 2, 3],
287
- 'string',
288
- 42,
289
- true,
290
- null,
291
- { nested: { deeply: { value: 'test' } } },
292
- [{ a: 1 }, { b: 2 }],
293
- { arr: [1, 2, { c: 3 }] }
294
- ];
295
-
296
- testCases.forEach(testCase => {
297
- const result = stableStringify(testCase);
298
- expect(() => JSON.parse(result)).not.toThrow();
299
- });
300
- });
301
-
302
- test('parsed output equals original structure', () => {
303
- const obj = {
304
- z: 3,
305
- a: 1,
306
- nested: {
307
- y: 2,
308
- x: 1
309
- },
310
- arr: [3, 2, 1]
311
- };
312
-
313
- const result = stableStringify(obj);
314
- const parsed = JSON.parse(result);
315
-
316
- expect(parsed).toEqual(obj);
317
- });
318
- });
319
-
320
- describe('stability comparison', () => {
321
- test('produces same hash for equivalent objects', () => {
322
- const obj1 = { b: 2, a: 1, c: { z: 26, y: 25 } };
323
- const obj2 = { c: { y: 25, z: 26 }, a: 1, b: 2 };
324
-
325
- expect(stableStringify(obj1)).toBe(stableStringify(obj2));
326
- });
327
-
328
- test('produces different output for different objects', () => {
329
- const obj1 = { a: 1, b: 2 };
330
- const obj2 = { a: 1, b: 3 };
331
-
332
- expect(stableStringify(obj1)).not.toBe(stableStringify(obj2));
333
- });
334
- });
335
- });