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