diff --git a/extension/src/popup/components/__tests__/settings.test.ts b/extension/src/popup/components/__tests__/settings.test.ts index edf3ca4..937554e 100644 --- a/extension/src/popup/components/__tests__/settings.test.ts +++ b/extension/src/popup/components/__tests__/settings.test.ts @@ -24,18 +24,35 @@ function mockChromeStorage(initial: Record = {}) { 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; } -function settingsResponses() { - // Two parallel calls in renderSettings: get_settings + get_blacklist. +// 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; @@ -45,42 +62,45 @@ describe('settings view', () => { (sendMessage as ReturnType).mockReset(); }); - it('renders a Sync now button', async () => { + it('renders the left-nav with the seven sections', async () => { mockChromeStorage(); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); - expect(app.querySelector('#sync-now-btn')).not.toBeNull(); + const sections = ['autofill', 'display', 'security', 'generator', 'retention', 'backup', 'import']; + for (const s of sections) { + expect(app.querySelector(`[data-section="${s}"]`)).not.toBeNull(); + } }); - it('clicking Sync now sends a sync message and shows feedback on success', async () => { + it('lands on the Autofill section by default and renders the capture toggle', async () => { mockChromeStorage(); - settingsResponses(); + 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); - app.querySelector('#sync-now-btn')!.click(); - await new Promise((r) => setTimeout(r, 0)); + 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: 'sync' }); - const status = app.querySelector('#sync-status')!; - expect(status.textContent).toMatch(/synced/i); - }); - - it('shows the error when sync fails', async () => { - mockChromeStorage(); - settingsResponses(); - (sendMessage as ReturnType).mockResolvedValueOnce({ ok: false, error: 'remote_unreachable' }); - - await renderSettings(app); - app.querySelector('#sync-now-btn')!.click(); - await new Promise((r) => setTimeout(r, 0)); - await new Promise((r) => setTimeout(r, 0)); - - const status = app.querySelector('#sync-status')!; - expect(status.textContent).toMatch(/remote_unreachable/); + expect(sendMessage).toHaveBeenCalledWith({ + type: 'update_settings', + settings: { captureEnabled: true }, + }); }); }); @@ -95,12 +115,13 @@ describe('settings Display section', () => { it('renders digit and symbol color pickers with default values when storage is empty', async () => { mockChromeStorage(); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - const digitInput = app.querySelector('#display-digit-color'); - const symbolInput = app.querySelector('#display-symbol-color'); + 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); @@ -111,32 +132,35 @@ describe('settings Display section', () => { mockChromeStorage({ password_display_scheme: { digit_color: '#112233', symbol_color: '#aabbcc' }, }); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - const digitInput = app.querySelector('#display-digit-color'); - const symbolInput = app.querySelector('#display-symbol-color'); + 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 () => { + it('renders a color-preview swatch element', async () => { mockChromeStorage(); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - expect(app.querySelector('#display-swatch')).not.toBeNull(); + expect(app.querySelector('#color-preview')).not.toBeNull(); }); it('changing digit color calls saveColorScheme with updated scheme', async () => { mockChromeStorage(); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - const digitInput = app.querySelector('#display-digit-color')!; + const digitInput = app.querySelector('#digit-color')!; digitInput.value = '#ff0000'; digitInput.dispatchEvent(new Event('change')); await new Promise((r) => setTimeout(r, 0)); @@ -151,11 +175,12 @@ describe('settings Display section', () => { it('changing symbol color calls saveColorScheme with updated scheme', async () => { mockChromeStorage(); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - const symbolInput = app.querySelector('#display-symbol-color')!; + const symbolInput = app.querySelector('#symbol-color')!; symbolInput.value = '#00ff00'; symbolInput.dispatchEvent(new Event('change')); await new Promise((r) => setTimeout(r, 0)); @@ -172,19 +197,21 @@ describe('settings Display section', () => { mockChromeStorage({ password_display_scheme: { digit_color: '#112233', symbol_color: '#aabbcc' }, }); - settingsResponses(); + mockDefaultLanding(); await renderSettings(app); + await navigateToDisplay(app); - const resetBtn = app.querySelector('#display-reset')!; + 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('#display-digit-color')!; - const symbolInput = app.querySelector('#display-symbol-color')!; + 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); });