@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,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
|
-
});
|