Files
2026-07-13 12:34:04 +08:00

265 lines
9.5 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {}, activeElement: null, documentElement: { style: {} }, body: { classList: { remove: () => {} } }, querySelectorAll: () => [], createElement: () => ({}) };
globalThis.TabOutIconUtils = {};
globalThis.TabOutBackgroundImage = {};
globalThis.TabOutListOrder = { reorderSubsetByIds: (a, b) => a };
globalThis.TabHarborTodos = { load: async () => [], save: async () => {} };
globalThis.TabHarborDashboardRuntime = null;
globalThis.chrome = { runtime: { lastError: null }, storage: { local: { get: async () => ({}), set: async () => {} } } };
globalThis.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
globalThis.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
globalThis.window = {
matchMedia: query => ({
media: query,
matches: false,
addEventListener: () => {},
removeEventListener: () => {},
}),
};
require('./theme-controls.js');
const {
filterRealTabs,
getResolvedThemeDefinition,
getResolvedTone,
normalizeShortcutUrl,
normalizeQuickShortcuts,
normalizeThemePreferences,
} = globalThis.TabOutThemeControls;
// ---- normalizeThemePreferences / resolved theme ----
test('normalizeThemePreferences migrates legacy midnight to dark mist', () => {
const result = normalizeThemePreferences({ themeId: 'midnight', surfaceOpacity: 19 });
assert.equal(result.mode, 'dark');
assert.equal(result.paletteId, 'mist');
assert.equal(result.surfaceOpacity, 19);
});
test('normalizeThemePreferences migrates legacy light theme ids to light palette families', () => {
const result = normalizeThemePreferences({ themeId: 'sage' });
assert.equal(result.mode, 'light');
assert.equal(result.paletteId, 'sage');
});
test('normalizeThemePreferences keeps explicit mode and palette values', () => {
const result = normalizeThemePreferences({
mode: 'system',
paletteId: 'blush',
surfaceOpacity: 9,
uiScale: 112,
shortcutScale: 124,
savedSessionRestoreMode: 'current-window',
savedSessionNavDisplayMode: 'icon',
});
assert.equal(result.mode, 'system');
assert.equal(result.paletteId, 'blush');
assert.equal(result.surfaceOpacity, 9);
assert.equal(result.uiScale, 112);
assert.equal(result.shortcutScale, 124);
assert.equal(result.savedSessionRestoreMode, 'current-window');
assert.equal(result.savedSessionNavDisplayMode, 'icon');
});
test('normalizeThemePreferences clamps size controls to calm readable ranges', () => {
const small = normalizeThemePreferences({ uiScale: 40, shortcutScale: 40 });
assert.equal(small.uiScale, 100);
assert.equal(small.shortcutScale, 100);
const large = normalizeThemePreferences({ uiScale: 180, shortcutScale: 180 });
assert.equal(large.uiScale, 120);
assert.equal(large.shortcutScale, 130);
});
test('normalizeThemePreferences defaults saved session restore mode to new-window', () => {
const result = normalizeThemePreferences({});
assert.equal(result.savedSessionRestoreMode, 'new-window');
assert.equal(result.savedSessionNavDisplayMode, 'name');
});
test('normalizeThemePreferences falls back to new-window for invalid saved session restore mode', () => {
const result = normalizeThemePreferences({
savedSessionRestoreMode: 'hidden-window',
savedSessionNavDisplayMode: 'label',
});
assert.equal(result.savedSessionRestoreMode, 'new-window');
assert.equal(result.savedSessionNavDisplayMode, 'name');
});
test('normalizeThemePreferences defaults closeDuplicateNewTabsEnabled to false', () => {
const result = normalizeThemePreferences({});
assert.equal(result.closeDuplicateNewTabsEnabled, false);
});
test('normalizeThemePreferences preserves closeDuplicateNewTabsEnabled when true', () => {
const result = normalizeThemePreferences({ closeDuplicateNewTabsEnabled: true });
assert.equal(result.closeDuplicateNewTabsEnabled, true);
});
test('normalizeThemePreferences rejects non-boolean closeDuplicateNewTabsEnabled', () => {
assert.equal(normalizeThemePreferences({ closeDuplicateNewTabsEnabled: 'yes' }).closeDuplicateNewTabsEnabled, false);
assert.equal(normalizeThemePreferences({ closeDuplicateNewTabsEnabled: 1 }).closeDuplicateNewTabsEnabled, false);
assert.equal(normalizeThemePreferences({ closeDuplicateNewTabsEnabled: null }).closeDuplicateNewTabsEnabled, false);
});
test('getResolvedTone follows system preference when mode is system', () => {
const originalMatchMedia = globalThis.window.matchMedia;
globalThis.window.matchMedia = query => ({
media: query,
matches: true,
addEventListener: () => {},
removeEventListener: () => {},
});
try {
assert.equal(getResolvedTone({ mode: 'system' }), 'dark');
} finally {
globalThis.window.matchMedia = originalMatchMedia;
}
});
test('getResolvedThemeDefinition resolves dark tokens from palette family', () => {
const theme = getResolvedThemeDefinition({ mode: 'dark', paletteId: 'paper' });
assert.equal(theme.name, 'Paper');
assert.equal(theme.tone, 'dark');
assert.equal(theme.vars['--paper'], '#1a1613');
});
// ---- filterRealTabs ----
test('filterRealTabs removes chrome:// internal pages', () => {
const tabs = [
{ id: 1, url: 'https://github.com' },
{ id: 2, url: 'chrome://newtab' },
{ id: 3, url: 'chrome://settings' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
});
test('filterRealTabs removes chrome-extension:// URLs', () => {
const tabs = [
{ id: 1, url: 'https://example.com' },
{ id: 2, url: 'chrome-extension://abc123/background.html' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
});
test('filterRealTabs removes about:blank and about:s pages', () => {
const tabs = [
{ id: 1, url: 'https://example.com' },
{ id: 2, url: 'about:blank' },
{ id: 3, url: 'about:settings' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
});
test('filterRealTabs removes edge:// and brave:// URLs', () => {
const tabs = [
{ id: 1, url: 'https://example.com' },
{ id: 2, url: 'edge://settings' },
{ id: 3, url: 'brave://rewards' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
});
test('filterRealTabs preserves tabs with no url field', () => {
const tabs = [
{ id: 1, title: 'GitHub' },
{ id: 2, url: 'https://github.com' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 2);
});
test('filterRealTabs is case-insensitive for protocol prefix', () => {
const tabs = [
{ id: 1, url: 'CHROME://settings' },
{ id: 2, url: 'Chrome-extension://abc' },
];
const result = filterRealTabs(tabs);
assert.equal(result.length, 2); // url.startsWith is case-sensitive in JS
});
test('filterRealTabs handles empty array', () => {
assert.equal(filterRealTabs([]).length, 0);
});
// ---- normalizeShortcutUrl ----
test('normalizeShortcutUrl strips leading/trailing whitespace', () => {
// URL() normalizes to canonical form (adds trailing slash for bare domains)
assert.equal(normalizeShortcutUrl(' https://example.com '), 'https://example.com/');
assert.equal(normalizeShortcutUrl(' https://github.com/user '), 'https://github.com/user');
});
test('normalizeShortcutUrl handles empty input', () => {
assert.equal(normalizeShortcutUrl(''), '');
assert.equal(normalizeShortcutUrl(' '), '');
});
test('normalizeShortcutUrl preserves valid URLs', () => {
assert.equal(normalizeShortcutUrl('https://github.com/user/repo'), 'https://github.com/user/repo');
});
test('normalizeShortcutUrl adds https for host-only URLs', () => {
assert.equal(normalizeShortcutUrl('github.com'), 'https://github.com/');
assert.equal(normalizeShortcutUrl('example.org/path'), 'https://example.org/path');
});
test('normalizeShortcutUrl supports internationalized domains and paths', () => {
assert.equal(
normalizeShortcutUrl('例子.测试/路径'),
'https://xn--fsqu00a.xn--0zwm56d/%E8%B7%AF%E5%BE%84'
);
});
// ---- normalizeQuickShortcuts ----
test('normalizeQuickShortcuts ignores non-array input', () => {
assert.deepEqual(normalizeQuickShortcuts(null), []);
assert.deepEqual(normalizeQuickShortcuts(undefined), []);
assert.deepEqual(normalizeQuickShortcuts('not an array'), []);
});
test('normalizeQuickShortcuts filters shortcuts missing url only', () => {
const input = [
{ id: 'a', url: 'https://a.com' },
{ id: 'b', url: '' },
{ id: '', url: 'https://c.com' }, // id missing is ok, id is auto-generated
];
const result = normalizeQuickShortcuts(input);
assert.equal(result.length, 2);
});
test('normalizeQuickShortcuts defaults iconKind to empty string for no icon', () => {
const input = [{ id: 's1', url: 'https://ex.com' }];
const result = normalizeQuickShortcuts(input);
assert.equal(result[0].iconKind, '');
});
test('normalizeQuickShortcuts infers iconKind from icon content', () => {
const input = [{ id: 's1', url: 'https://ex.com', icon: '🌟' }];
const result = normalizeQuickShortcuts(input);
assert.equal(result[0].iconKind, 'glyph');
assert.equal(result[0].icon, '🌟');
});
test('normalizeQuickShortcuts normalizes icon URL to image kind', () => {
const input = [{ id: 's1', url: 'https://ex.com', icon: 'https://ex.com/f.png' }];
const result = normalizeQuickShortcuts(input);
assert.equal(result[0].iconKind, 'image');
assert.equal(result[0].icon, 'https://ex.com/f.png');
});