@thumbmarkjs/thumbmarkjs
Version:
   • 9.89 kB
text/typescript
import { getExcludeList, filterThumbmarkData } from './filterComponents';
import { defaultOptions } from '../options';
import { componentInterface } from '../factory';
// Mock getBrowser so we can control browser detection in tests
jest.mock('../components/system/browser', () => ({
getBrowser: jest.fn(() => ({ name: 'unknown', version: 'unknown' })),
}));
import { getBrowser } from '../components/system/browser';
const mockGetBrowser = getBrowser as jest.Mock;
const testData: componentInterface = {
one: '1',
two: 2,
three: { a: true, b: false },
speech: { hash: 'abc' },
canvas: { hash: 'def' },
audio: { sampleHash: 'ghi', other: 'jkl' },
};
// ── getExcludeList ──────────────────────────────────────────────
describe('getExcludeList', () => {
beforeEach(() => {
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
});
test('returns empty when no options and unknown browser', () => {
expect(getExcludeList()).toEqual([]);
});
test('returns user exclude list as-is', () => {
const result = getExcludeList({ ...defaultOptions, exclude: ['one', 'two'] });
expect(result).toContain('one');
expect(result).toContain('two');
});
test("'always' rules apply even with empty stabilize", () => {
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '130.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: [] });
expect(result).toContain('speech');
});
test("'always' rules don't apply when browser doesn't match", () => {
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: [] });
expect(result).not.toContain('speech');
});
test('stabilization rules expand for matching browser', () => {
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
expect(result).toContain('canvas');
expect(result).toContain('fonts');
expect(result).toContain('tls.extensions');
});
test('stabilization rules do not apply for non-matching browser', () => {
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
expect(result).not.toContain('canvas');
expect(result).not.toContain('fonts');
// Chrome does get some rules though
expect(result).toContain('header.acceptLanguage');
expect(result).toContain('tls.extensions');
});
test('version matching with >= syntax', () => {
// safari>=17 should match Safari 18
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '18.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
expect(result).toContain('canvas');
// safari>=17 should match Safari 17
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '17.0' });
const result17 = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
expect(result17).toContain('canvas');
// safari>=17 should NOT match Safari 16
mockGetBrowser.mockReturnValue({ name: 'Safari', version: '16.5' });
const result16 = getExcludeList({ ...defaultOptions, stabilize: ['private'] });
expect(result16).not.toContain('canvas');
});
test('browser-independent rules always apply', () => {
// 'iframe' has { exclude: ['permissions'] } with no browsers key
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['iframe'] });
expect(result).toContain('permissions');
});
test('vpn stabilization excludes ip for any browser', () => {
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['vpn'] });
expect(result).toContain('ip');
});
test('multiple stabilization options combine', () => {
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, stabilize: ['private', 'vpn'] });
expect(result).toContain('canvas'); // from private+firefox
expect(result).toContain('ip'); // from vpn
expect(result).toContain('speech'); // from always+firefox
});
test('user exclude list is combined with stabilization rules', () => {
mockGetBrowser.mockReturnValue({ name: 'Firefox', version: '120.0' });
const result = getExcludeList({ ...defaultOptions, exclude: ['custom'], stabilize: ['private'] });
expect(result).toContain('custom'); // user's
expect(result).toContain('canvas'); // from rules
});
test('unknown stabilization option is silently skipped', () => {
const result = getExcludeList({ ...defaultOptions, stabilize: ['nonexistent' as any] });
// Should not throw, just return whatever 'always' contributes (nothing for unknown browser)
expect(Array.isArray(result)).toBe(true);
});
test('browser fallback from component data (server-side)', () => {
// getBrowser returns unknown (simulating server-side)
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
const obj: componentInterface = {
system: {
browser: {
name: 'Brave',
version: '130.0',
},
},
};
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] }, obj);
// Brave-specific rules should apply via fallback
expect(result).toContain('canvas');
expect(result).toContain('plugins');
expect(result).toContain('speech'); // from 'always'
});
test('browser fallback is skipped when getBrowser succeeds', () => {
mockGetBrowser.mockReturnValue({ name: 'Chrome', version: '120.0' });
const obj: componentInterface = {
system: {
browser: {
name: 'Firefox',
version: '120.0',
},
},
};
const result = getExcludeList({ ...defaultOptions, stabilize: ['private'] }, obj);
// Should use Chrome rules (from getBrowser), not Firefox (from obj)
expect(result).toContain('header.acceptLanguage'); // Chrome rule
expect(result).not.toContain('fonts'); // Firefox-only rule
});
test('browser fallback handles missing system gracefully', () => {
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
const obj: componentInterface = { one: '1' };
// Should not throw
expect(() => getExcludeList({ ...defaultOptions }, obj)).not.toThrow();
});
});
// ── filterThumbmarkData ─────────────────────────────────────────
describe('filterThumbmarkData', () => {
beforeEach(() => {
mockGetBrowser.mockReturnValue({ name: 'unknown', version: 'unknown' });
});
test('returns full object with no options', () => {
const result = filterThumbmarkData(testData);
expect(result).toEqual(testData);
});
test('returns full object with empty exclude', () => {
const result = filterThumbmarkData(testData, { ...defaultOptions, exclude: [], stabilize: [] });
expect(result).toEqual(testData);
});
test('include overrides exclude for the same key', () => {
const result = filterThumbmarkData(testData, {
...defaultOptions,
exclude: ['one'],
include: ['one'],
stabilize: [],
});
expect(result.one).toBe('1');
});
test('include overrides exclude at nested level', () => {
const result = filterThumbmarkData(testData, {
...defaultOptions,
exclude: ['three'],
include: ['three.a'],
stabilize: [],
});
expect(result.three).toEqual({ a: true });
expect((result.three as componentInterface).b).toBeUndefined();
});
test('excluding a parent removes all children', () => {
const result = filterThumbmarkData(testData, {
...defaultOptions,
exclude: ['three'],
stabilize: [],
});
expect(result.three).toBeUndefined();
});
test('stabilization rules integrate with filterThumbmarkData', () => {
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
const result = filterThumbmarkData(testData, {
...defaultOptions,
stabilize: ['private'],
});
// Brave+private excludes canvas
expect(result.canvas).toBeUndefined();
// Brave+'always' excludes speech
expect(result.speech).toBeUndefined();
// Others remain
expect(result.one).toBe('1');
});
test('nested exclusion via stabilization rules', () => {
mockGetBrowser.mockReturnValue({ name: 'Brave', version: '130.0' });
const result = filterThumbmarkData(testData, {
...defaultOptions,
stabilize: ['private'],
});
// audio.sampleHash excluded for Brave, but audio.other should remain
expect(result.audio).toBeDefined();
expect((result.audio as componentInterface).other).toBe('jkl');
expect((result.audio as componentInterface).sampleHash).toBeUndefined();
});
});