@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,80 +0,0 @@
1
- import getHardware from './index';
2
-
3
- describe('hardware component tests', () => {
4
- let originalGetContext: any;
5
-
6
- beforeAll(() => {
7
- originalGetContext = HTMLCanvasElement.prototype.getContext;
8
- });
9
-
10
- afterAll(() => {
11
- HTMLCanvasElement.prototype.getContext = originalGetContext;
12
- });
13
-
14
- test("getHardware returns valid structure even when WebGL is blocked", async () => {
15
- // Mock getContext to return null (simulating WebGL being disabled/blocked)
16
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(null);
17
-
18
- const result = await getHardware();
19
-
20
- expect(result).toBeDefined();
21
- if (result) {
22
- expect(result.videocard).toBe("undefined");
23
- expect(result.deviceMemory).toBeDefined();
24
- expect(result.architecture).toBeDefined();
25
- }
26
- });
27
-
28
- test("getHardware handles WebGL getParameter throwing gracefully", async () => {
29
- // Mock a broken WebGL context
30
- const mockGl = {
31
- getParameter: jest.fn().mockImplementation(() => {
32
- throw new Error("WebGL error");
33
- }),
34
- getExtension: jest.fn().mockReturnValue(null),
35
- VENDOR: 1,
36
- RENDERER: 2,
37
- VERSION: 3,
38
- SHADING_LANGUAGE_VERSION: 4
39
- };
40
-
41
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(mockGl);
42
-
43
- const result = await getHardware();
44
-
45
- expect(result).toBeDefined();
46
- if (result) {
47
- expect(result.videocard).toBe("undefined");
48
- }
49
- });
50
-
51
- test("getHardware handles WebGL getExtension returning null", async () => {
52
- // Mock a context where getParameter works but getExtension (debug info) returns null
53
- const mockGl = {
54
- getParameter: jest.fn().mockImplementation((param) => {
55
- if (param === 1) return "Mock Vendor"; // VENDOR
56
- if (param === 2) return ""; // RENDERER (empty to trigger getExtension check)
57
- return "Mock Value";
58
- }),
59
- getExtension: jest.fn().mockReturnValue(null),
60
- VENDOR: 1,
61
- RENDERER: 2,
62
- VERSION: 3,
63
- SHADING_LANGUAGE_VERSION: 4
64
- };
65
-
66
- HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue(mockGl);
67
-
68
- const result = await getHardware();
69
-
70
- expect(result).toBeDefined();
71
- if (result) {
72
- expect(result.videocard).toBeDefined();
73
- const videocard = result.videocard as any;
74
- expect(videocard.vendor).toBe("Mock Vendor");
75
- expect(videocard.renderer).toBe("");
76
- // Should not have unmasked fields
77
- expect(videocard.vendorUnmasked).toBeUndefined();
78
- }
79
- });
80
- });
@@ -1,124 +0,0 @@
1
- import getIntl from './index';
2
-
3
- describe('intl component tests', () => {
4
- const originalIntl = globalThis.Intl;
5
-
6
- afterEach(() => {
7
- Object.defineProperty(globalThis, 'Intl', {
8
- value: originalIntl,
9
- configurable: true,
10
- writable: true,
11
- });
12
- });
13
-
14
- test('returns valid structure with all expected keys', async () => {
15
- const result = await getIntl();
16
-
17
- expect(result).not.toBeNull();
18
- expect(result).toHaveProperty('hash');
19
- expect(result).toHaveProperty('details');
20
- expect(typeof result!.hash).toBe('string');
21
- const details = result!.details as Record<string, unknown>;
22
- const coreKeys = [
23
- 'dateFullFormat', 'dateMediumFormat', 'timeFormat',
24
- 'numberFormat', 'currencyFormat', 'percentFormat',
25
- 'nonLatinDate', 'nonLatinNumber',
26
- ];
27
- for (const key of coreKeys) {
28
- expect(details).toHaveProperty(key);
29
- expect(typeof details[key]).toBe('string');
30
- }
31
- });
32
-
33
- test('returns null when Intl is unavailable', async () => {
34
- Object.defineProperty(globalThis, 'Intl', {
35
- value: undefined,
36
- configurable: true,
37
- writable: true,
38
- });
39
-
40
- const result = await getIntl();
41
- expect(result).toBeNull();
42
- });
43
-
44
- test('omits listFormat when ListFormat is unavailable', async () => {
45
- const mockIntl = Object.create(originalIntl);
46
- Object.defineProperty(mockIntl, 'ListFormat', {
47
- value: undefined,
48
- configurable: true,
49
- });
50
- Object.defineProperty(globalThis, 'Intl', {
51
- value: mockIntl,
52
- configurable: true,
53
- writable: true,
54
- });
55
-
56
- const result = await getIntl();
57
- const details = result!.details as Record<string, unknown>;
58
-
59
- expect(result).not.toBeNull();
60
- expect(details).toHaveProperty('dateFullFormat');
61
- expect(details).not.toHaveProperty('listFormat');
62
- });
63
-
64
- test('omits displayNames when DisplayNames is unavailable', async () => {
65
- const mockIntl = Object.create(originalIntl);
66
- Object.defineProperty(mockIntl, 'DisplayNames', {
67
- value: undefined,
68
- configurable: true,
69
- });
70
- Object.defineProperty(globalThis, 'Intl', {
71
- value: mockIntl,
72
- configurable: true,
73
- writable: true,
74
- });
75
-
76
- const result = await getIntl();
77
- const details = result!.details as Record<string, unknown>;
78
-
79
- expect(result).not.toBeNull();
80
- expect(details).toHaveProperty('dateFullFormat');
81
- expect(details).not.toHaveProperty('displayNames');
82
- });
83
-
84
- test('includes guarded probes when available', async () => {
85
- const result = await getIntl();
86
- const details = result!.details as Record<string, unknown>;
87
-
88
- expect(result).not.toBeNull();
89
- if (typeof (Intl as any).ListFormat === 'function') {
90
- expect(details).toHaveProperty('listFormat');
91
- expect(typeof details.listFormat).toBe('string');
92
- }
93
- if (typeof (Intl as any).DisplayNames === 'function') {
94
- expect(details).toHaveProperty('displayNames');
95
- expect(typeof details.displayNames).toBe('string');
96
- }
97
- });
98
-
99
- test('returns null when Intl constructor throws', async () => {
100
- const mockIntl = Object.create(originalIntl);
101
- Object.defineProperty(mockIntl, 'DateTimeFormat', {
102
- value: function() { throw new Error('broken'); },
103
- configurable: true,
104
- });
105
- Object.defineProperty(globalThis, 'Intl', {
106
- value: mockIntl,
107
- configurable: true,
108
- writable: true,
109
- });
110
-
111
- const result = await getIntl();
112
- expect(result).toBeNull();
113
- });
114
-
115
- test('produces deterministic number format output', async () => {
116
- const result = await getIntl();
117
- const details = result!.details as Record<string, unknown>;
118
-
119
- expect(result).not.toBeNull();
120
- expect(details.numberFormat).toBe('123,456.789');
121
- expect(details.currencyFormat).toBe('$123,456.79');
122
- expect(details.percentFormat).toBe('46%');
123
- });
124
- });
@@ -1,41 +0,0 @@
1
- import { componentInterface } from '../../factory';
2
- import { hash } from '../../utils/hash';
3
- import { stableStringify } from '../../utils/stableStringify';
4
-
5
- export default async function getIntl(): Promise<componentInterface | null> {
6
- if (typeof Intl === 'undefined') {
7
- return null;
8
- }
9
-
10
- try {
11
- const date = new Date(Date.UTC(2024, 0, 15, 12, 30, 45));
12
-
13
- const details: componentInterface = {
14
- dateFullFormat: new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeZone: 'UTC' } as any).format(date),
15
- dateMediumFormat: new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' } as any).format(date),
16
- timeFormat: new Intl.DateTimeFormat('en-US', { timeStyle: 'long', timeZone: 'UTC' } as any).format(date),
17
- numberFormat: new Intl.NumberFormat('en-US').format(123456.789),
18
- currencyFormat: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(123456.789),
19
- percentFormat: new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.456),
20
- nonLatinDate: new Intl.DateTimeFormat('ar-EG', { timeZone: 'UTC' }).format(date),
21
- nonLatinNumber: new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(123456.789),
22
- };
23
-
24
- const IntlAny = Intl as any;
25
-
26
- if (typeof IntlAny.ListFormat === 'function') {
27
- details.listFormat = new IntlAny.ListFormat('en', { type: 'conjunction' }).format(['a', 'b', 'c']);
28
- }
29
-
30
- if (typeof IntlAny.DisplayNames === 'function') {
31
- details.displayNames = new IntlAny.DisplayNames('en', { type: 'region' }).of('US') || '';
32
- }
33
-
34
- return {
35
- details,
36
- hash: hash(stableStringify(details)),
37
- };
38
- } catch {
39
- return null;
40
- }
41
- }
@@ -1,120 +0,0 @@
1
- import getMediaDevices from './index';
2
-
3
- describe('mediaDevices component tests', () => {
4
- let originalMediaDevices: MediaDevices | undefined;
5
-
6
- beforeAll(() => {
7
- originalMediaDevices = navigator.mediaDevices;
8
- });
9
-
10
- afterAll(() => {
11
- Object.defineProperty(navigator, 'mediaDevices', {
12
- value: originalMediaDevices,
13
- configurable: true,
14
- });
15
- });
16
-
17
- function mockEnumerateDevices(devices: Array<{ kind: string }>) {
18
- Object.defineProperty(navigator, 'mediaDevices', {
19
- value: {
20
- enumerateDevices: jest.fn().mockResolvedValue(devices),
21
- },
22
- configurable: true,
23
- });
24
- }
25
-
26
- test('returns valid structure when API is available', async () => {
27
- mockEnumerateDevices([
28
- { kind: 'audioinput' },
29
- { kind: 'audiooutput' },
30
- { kind: 'videoinput' },
31
- ]);
32
-
33
- const result = await getMediaDevices();
34
-
35
- expect(result).toEqual({
36
- audioinput: 1,
37
- audiooutput: 1,
38
- videoinput: 1,
39
- });
40
- });
41
-
42
- test('returns null when mediaDevices API is unavailable', async () => {
43
- Object.defineProperty(navigator, 'mediaDevices', {
44
- value: undefined,
45
- configurable: true,
46
- });
47
-
48
- const result = await getMediaDevices();
49
- expect(result).toBeNull();
50
- });
51
-
52
- test('returns null when enumerateDevices throws', async () => {
53
- Object.defineProperty(navigator, 'mediaDevices', {
54
- value: {
55
- enumerateDevices: jest.fn().mockRejectedValue(new Error('NotAllowedError')),
56
- },
57
- configurable: true,
58
- });
59
-
60
- const result = await getMediaDevices();
61
- expect(result).toBeNull();
62
- });
63
-
64
- test('handles empty device list', async () => {
65
- mockEnumerateDevices([]);
66
-
67
- const result = await getMediaDevices();
68
-
69
- expect(result).toEqual({
70
- audioinput: 0,
71
- audiooutput: 0,
72
- videoinput: 0,
73
- });
74
- });
75
-
76
- test('counts multiple devices of same kind', async () => {
77
- mockEnumerateDevices([
78
- { kind: 'videoinput' },
79
- { kind: 'videoinput' },
80
- { kind: 'audioinput' },
81
- ]);
82
-
83
- const result = await getMediaDevices();
84
-
85
- expect(result).toEqual({
86
- audioinput: 1,
87
- audiooutput: 0,
88
- videoinput: 2,
89
- });
90
- });
91
-
92
- test('ignores unknown device kind values', async () => {
93
- mockEnumerateDevices([
94
- { kind: 'audioinput' },
95
- { kind: '' },
96
- { kind: 'somethingelse' },
97
- { kind: 'videoinput' },
98
- ]);
99
-
100
- const result = await getMediaDevices();
101
-
102
- expect(result).toEqual({
103
- audioinput: 1,
104
- audiooutput: 0,
105
- videoinput: 1,
106
- });
107
- });
108
-
109
- test('returns null when enumerateDevices resolves with null', async () => {
110
- Object.defineProperty(navigator, 'mediaDevices', {
111
- value: {
112
- enumerateDevices: jest.fn().mockResolvedValue(null),
113
- },
114
- configurable: true,
115
- });
116
-
117
- const result = await getMediaDevices();
118
- expect(result).toBeNull();
119
- });
120
- });
@@ -1,26 +0,0 @@
1
- import { componentInterface } from '../../factory';
2
-
3
- export default async function getMediaDevices(): Promise<componentInterface | null> {
4
- if (typeof navigator === 'undefined' ||
5
- !navigator.mediaDevices ||
6
- typeof navigator.mediaDevices.enumerateDevices !== 'function') {
7
- return null;
8
- }
9
-
10
- try {
11
- const devices = await navigator.mediaDevices.enumerateDevices();
12
-
13
- const counts: Record<string, number> = {};
14
- for (const device of devices) {
15
- counts[device.kind] = (counts[device.kind] || 0) + 1;
16
- }
17
-
18
- return {
19
- audioinput: counts['audioinput'] || 0,
20
- audiooutput: counts['audiooutput'] || 0,
21
- videoinput: counts['videoinput'] || 0,
22
- };
23
- } catch {
24
- return null;
25
- }
26
- }
@@ -1,108 +0,0 @@
1
- import { getBrowser } from './browser';
2
-
3
- describe('getBrowser', () => {
4
- const originalNavigator = global.navigator;
5
-
6
- function mockUserAgent(ua: string) {
7
- Object.defineProperty(global, 'navigator', {
8
- value: { userAgent: ua },
9
- configurable: true,
10
- });
11
- }
12
-
13
- afterAll(() => {
14
- // Restore original navigator
15
- Object.defineProperty(global, 'navigator', {
16
- value: originalNavigator,
17
- configurable: true,
18
- });
19
- });
20
-
21
- // Authoritative user agent test cases from DeviceAtlas
22
- const cases = [
23
- // Chrome (Windows)
24
- {
25
- ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36',
26
- expected: { name: 'Chrome', version: '120.0.6099.110' },
27
- },
28
- // Firefox (Windows)
29
- {
30
- ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
31
- expected: { name: 'Firefox', version: '120.0' },
32
- },
33
- // Edge (Windows)
34
- {
35
- ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 Edg/120.0.2210.61',
36
- expected: { name: 'Edge', version: '120.0.2210.61' },
37
- },
38
- // Safari (macOS)
39
- {
40
- ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
41
- expected: { name: 'Safari', version: '17.1' },
42
- },
43
- // Chrome (macOS)
44
- {
45
- ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36',
46
- expected: { name: 'Chrome', version: '120.0.6099.110' },
47
- },
48
- // Firefox (macOS)
49
- {
50
- ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
51
- expected: { name: 'Firefox', version: '120.0' },
52
- },
53
- // Chrome (Android)
54
- {
55
- ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Mobile Safari/537.36',
56
- expected: { name: 'Chrome', version: '120.0.6099.110' },
57
- },
58
- // Samsung Internet (Android)
59
- {
60
- ua: 'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/120.0.6099.110 Mobile Safari/537.36',
61
- expected: { name: 'SamsungBrowser', version: '23.0' },
62
- },
63
- // Firefox (Android)
64
- {
65
- ua: 'Mozilla/5.0 (Android 13; Mobile; rv:120.0) Gecko/120.0 Firefox/120.0',
66
- expected: { name: 'Firefox', version: '120.0' },
67
- },
68
- // Safari (iPhone)
69
- {
70
- ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
71
- expected: { name: 'Safari', version: '17.1' },
72
- },
73
- // Chrome (iPhone)
74
- {
75
- ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.110 Mobile/15E148 Safari/604.1',
76
- expected: { name: 'Chrome', version: '120.0.6099.110' },
77
- },
78
- // Firefox (iPhone)
79
- {
80
- ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148 Safari/604.1',
81
- expected: { name: 'Firefox', version: '120.0' },
82
- },
83
- // Opera (Windows)
84
- {
85
- ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 OPR/107.0.5045.36',
86
- expected: { name: 'Opera', version: '107.0.5045.36' },
87
- },
88
- // Vivaldi (Windows)
89
- {
90
- ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 Vivaldi/6.4.3160.49',
91
- expected: { name: 'Vivaldi', version: '6.4.3160.49' },
92
- },
93
- // Unknown
94
- {
95
- ua: 'SomeRandomUserAgent/1.0',
96
- expected: { name: 'SomeRandomUserAgent', version: '1.0' },
97
- },
98
- ];
99
-
100
- cases.forEach(({ ua, expected }) => {
101
- it(`should detect ${expected.name} ${expected.version} from UA: ${ua}`, () => {
102
- mockUserAgent(ua);
103
- const result = getBrowser();
104
- expect(result.name.toLowerCase()).toBe(expected.name.toLowerCase());
105
- expect(result.version).toContain(expected.version.split('.')[0]); // loose match for version
106
- });
107
- });
108
- });