import { beforeEach, describe, expect, it, vi } from 'vitest'; import { renderSettings } from '../settings'; vi.mock('../../../shared/state', () => ({ setState: vi.fn(), sendMessage: vi.fn(), navigate: vi.fn(), escapeHtml: (s: string) => s, popOutToTab: vi.fn(), isInTab: vi.fn(() => false), openVaultTab: vi.fn(), })); import { sendMessage } from '../../../shared/state'; import { DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR } from '../../../shared/color-scheme'; function mockChromeStorage(initial: Record = {}) { const store: Record = { ...initial }; (global as any).chrome = { storage: { sync: { get: vi.fn((key: string) => Promise.resolve( key in store ? { [key]: store[key] } : {})), set: vi.fn((kv: Record) => { Object.assign(store, kv); return Promise.resolve(); }), remove: vi.fn((key: string) => { delete store[key]; return Promise.resolve(); }), }, local: { get: vi.fn(() => Promise.resolve({})), set: vi.fn(() => Promise.resolve()), }, }, }; return store; } // After the Stream B left-nav restructure (bd6a301) and the management-surfaces // revamp, renderSettings makes these calls in this order: // 1. is_unlocked (gates vault-only sections) // 2. get_settings + get_blacklist (parallel) (Autofill is the default section) function mockDefaultLanding(opts: { unlocked?: boolean } = {}) { const unlocked = opts.unlocked ?? false; (sendMessage as ReturnType) .mockResolvedValueOnce({ ok: true, data: { unlocked } }) .mockResolvedValueOnce({ ok: true, data: { settings: { captureEnabled: false, captureStyle: 'bar' } } }) .mockResolvedValueOnce({ ok: true, data: { blacklist: [] } }); } async function navigateToDisplay(app: HTMLElement): Promise { const btn = app.querySelector('[data-section="display"]')!; btn.click(); // Allow renderDisplaySection's async loadColorScheme + DOM writes to settle. await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0)); } describe('settings view', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '
'; app = document.getElementById('app')!; (sendMessage as ReturnType).mockReset(); }); it('renders the left-nav with the seven sections', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); const sections = ['autofill', 'display', 'security', 'generator', 'retention', 'backup', 'import']; for (const s of sections) { expect(app.querySelector(`[data-section="${s}"]`)).not.toBeNull(); } }); it('lands on the Autofill section by default and renders the capture toggle', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); expect(sendMessage).toHaveBeenCalledWith({ type: 'is_unlocked' }); expect(sendMessage).toHaveBeenCalledWith({ type: 'get_settings' }); expect(sendMessage).toHaveBeenCalledWith({ type: 'get_blacklist' }); expect(app.querySelector('#capture-enabled')).not.toBeNull(); }); it('toggling capture-enabled sends an update_settings message', async () => { mockChromeStorage(); mockDefaultLanding(); (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true }); await renderSettings(app); const cb = app.querySelector('#capture-enabled')!; cb.checked = true; cb.dispatchEvent(new Event('change')); await new Promise((r) => setTimeout(r, 0)); expect(sendMessage).toHaveBeenCalledWith({ type: 'update_settings', settings: { captureEnabled: true }, }); }); }); describe('settings Display section', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '
'; app = document.getElementById('app')!; (sendMessage as ReturnType).mockReset(); }); it('renders digit and symbol color pickers with default values when storage is empty', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); const digitInput = app.querySelector('#digit-color'); const symbolInput = app.querySelector('#symbol-color'); expect(digitInput).not.toBeNull(); expect(symbolInput).not.toBeNull(); expect(digitInput!.value).toBe(DEFAULT_DIGIT_COLOR); expect(symbolInput!.value).toBe(DEFAULT_SYMBOL_COLOR); }); it('renders pickers with stored values when storage has a scheme', async () => { mockChromeStorage({ password_display_scheme: { digit_color: '#112233', symbol_color: '#aabbcc' }, }); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); const digitInput = app.querySelector('#digit-color'); const symbolInput = app.querySelector('#symbol-color'); expect(digitInput!.value).toBe('#112233'); expect(symbolInput!.value).toBe('#aabbcc'); }); it('renders a color-preview swatch element', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); expect(app.querySelector('#color-preview')).not.toBeNull(); }); it('changing digit color calls saveColorScheme with updated scheme', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); const digitInput = app.querySelector('#digit-color')!; digitInput.value = '#ff0000'; digitInput.dispatchEvent(new Event('change')); await new Promise((r) => setTimeout(r, 0)); const syncSet = (global as any).chrome.storage.sync.set as ReturnType; expect(syncSet).toHaveBeenCalledWith( expect.objectContaining({ password_display_scheme: expect.objectContaining({ digit_color: '#ff0000' }), }), ); }); it('changing symbol color calls saveColorScheme with updated scheme', async () => { mockChromeStorage(); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); const symbolInput = app.querySelector('#symbol-color')!; symbolInput.value = '#00ff00'; symbolInput.dispatchEvent(new Event('change')); await new Promise((r) => setTimeout(r, 0)); const syncSet = (global as any).chrome.storage.sync.set as ReturnType; expect(syncSet).toHaveBeenCalledWith( expect.objectContaining({ password_display_scheme: expect.objectContaining({ symbol_color: '#00ff00' }), }), ); }); it('clicking reset calls chrome.storage.sync.remove and restores defaults', async () => { mockChromeStorage({ password_display_scheme: { digit_color: '#112233', symbol_color: '#aabbcc' }, }); mockDefaultLanding(); await renderSettings(app); await navigateToDisplay(app); const resetBtn = app.querySelector('#reset-colors')!; resetBtn.click(); await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0)); const syncRemove = (global as any).chrome.storage.sync.remove as ReturnType; expect(syncRemove).toHaveBeenCalledWith('password_display_scheme'); const digitInput = app.querySelector('#digit-color')!; const symbolInput = app.querySelector('#symbol-color')!; expect(digitInput.value).toBe(DEFAULT_DIGIT_COLOR); expect(symbolInput.value).toBe(DEFAULT_SYMBOL_COLOR); }); });