import test from 'node:test'; import assert from 'node:assert/strict'; import { createSettingsDraft, filterSettingsFields, setDraftValue, resetDraftPath, getDirtyOperations, toConfigDraftValue, toSettingsDisplayValue, } from './settings-model'; import type { ConfigSettingsField } from '../types/settings'; const fields: ConfigSettingsField[] = [ { id: 'subtitleStyle.autoPauseVideoOnHover', label: 'Pause on subtitle hover', description: 'Pause while hovering subtitles.', configPath: 'subtitleStyle.autoPauseVideoOnHover', category: 'behavior', section: 'Playback Behavior', control: 'boolean', defaultValue: true, restartBehavior: 'hot-reload', }, { id: 'ankiConnect.enabled', label: 'Enable AnkiConnect', description: 'Enable Anki integration.', configPath: 'ankiConnect.enabled', category: 'mining-anki', section: 'Connection', control: 'boolean', defaultValue: true, restartBehavior: 'restart', }, { id: 'subtitleStyle.fontSize', label: 'Font Size', description: 'Hidden behind CSS editor.', configPath: 'subtitleStyle.fontSize', category: 'appearance', section: 'Primary Subtitle Appearance', control: 'number', defaultValue: 35, restartBehavior: 'hot-reload', settingsHidden: true, }, ]; test('filterSettingsFields searches label, section, and config path', () => { assert.deepEqual( filterSettingsFields(fields, { category: 'behavior', query: 'hover' }).map( (field) => field.configPath, ), ['subtitleStyle.autoPauseVideoOnHover'], ); assert.deepEqual(filterSettingsFields(fields, { category: 'behavior', query: 'anki' }), []); assert.deepEqual( filterSettingsFields(fields, { query: 'anki' }).map((field) => field.configPath), ['ankiConnect.enabled'], ); assert.deepEqual( filterSettingsFields(fields, { query: 'pause hover' }).map((field) => field.configPath), ['subtitleStyle.autoPauseVideoOnHover'], ); assert.deepEqual(filterSettingsFields(fields, { query: 'pause anki' }), []); assert.deepEqual(filterSettingsFields(fields, { category: 'appearance', query: '' }), []); }); test('filterSettingsFields normalizes punctuation in query terms', () => { const nPlusOneFields: ConfigSettingsField[] = [ { id: 'ankiConnect.nPlusOne.enabled', label: 'Enable N+1', description: 'Highlight N+1 cards.', configPath: 'ankiConnect.nPlusOne.enabled', category: 'mining-anki', section: 'N+1', control: 'boolean', defaultValue: true, restartBehavior: 'hot-reload', }, ]; assert.deepEqual( filterSettingsFields(nPlusOneFields, { query: 'n+1' }).map((field) => field.configPath), ['ankiConnect.nPlusOne.enabled'], ); }); test('filterSettingsFields preserves non-Latin query terms', () => { const japaneseFields: ConfigSettingsField[] = [ { id: 'subtitleStyle.japaneseFontFamily', label: '日本語フォント', description: '字幕の表示に使う書体。', configPath: 'subtitleStyle.japaneseFontFamily', category: 'appearance', section: 'Primary Subtitle Appearance', control: 'text', defaultValue: '', restartBehavior: 'hot-reload', }, ]; assert.deepEqual( filterSettingsFields(japaneseFields, { query: '日本語' }).map((field) => field.configPath), ['subtitleStyle.japaneseFontFamily'], ); }); test('settings draft tracks dirty set and emits save operations', () => { const draft = createSettingsDraft({ 'subtitleStyle.autoPauseVideoOnHover': true, }); setDraftValue(draft, 'subtitleStyle.autoPauseVideoOnHover', false); assert.deepEqual(getDirtyOperations(draft), [ { op: 'set', path: 'subtitleStyle.autoPauseVideoOnHover', value: false, }, ]); setDraftValue(draft, 'subtitleStyle.autoPauseVideoOnHover', true); assert.deepEqual(getDirtyOperations(draft), []); }); test('settings draft emits reset operations for css-editor-owned legacy style paths', () => { const draft = createSettingsDraft({ 'subtitleStyle.css': {}, 'subtitleStyle.fontSize': 35, }); setDraftValue(draft, 'subtitleStyle.css', { 'font-size': '42px' }); resetDraftPath(draft, 'subtitleStyle.fontSize', undefined); assert.deepEqual(getDirtyOperations(draft), [ { op: 'set', path: 'subtitleStyle.css', value: { 'font-size': '42px' }, }, { op: 'reset', path: 'subtitleStyle.fontSize', }, ]); }); test('discord presence update interval displays seconds while saving milliseconds', () => { const path = 'discordPresence.updateIntervalMs'; assert.equal(toSettingsDisplayValue(path, 3000), 3); assert.equal(toConfigDraftValue(path, 2.5), 2500); }); test('websocket enabled select values save booleans instead of strings', () => { assert.equal(toSettingsDisplayValue('websocket.enabled', true), 'true'); assert.equal(toSettingsDisplayValue('websocket.enabled', false), 'false'); assert.equal(toConfigDraftValue('websocket.enabled', 'true'), true); assert.equal(toConfigDraftValue('websocket.enabled', 'false'), false); assert.equal(toConfigDraftValue('websocket.enabled', 'auto'), 'auto'); });