diff --git a/extension/src/popup/components/__tests__/settings.test.ts b/extension/src/popup/components/__tests__/settings.test.ts index 98262a9..edf3ca4 100644 --- a/extension/src/popup/components/__tests__/settings.test.ts +++ b/extension/src/popup/components/__tests__/settings.test.ts @@ -12,6 +12,22 @@ vi.mock('../../../shared/state', () => ({ })); 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(); }), + }, + }, + }; + return store; +} function settingsResponses() { // Two parallel calls in renderSettings: get_settings + get_blacklist. @@ -30,6 +46,7 @@ describe('settings view', () => { }); it('renders a Sync now button', async () => { + mockChromeStorage(); settingsResponses(); await renderSettings(app); @@ -38,6 +55,7 @@ describe('settings view', () => { }); it('clicking Sync now sends a sync message and shows feedback on success', async () => { + mockChromeStorage(); settingsResponses(); (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true }); @@ -52,6 +70,7 @@ describe('settings view', () => { }); it('shows the error when sync fails', async () => { + mockChromeStorage(); settingsResponses(); (sendMessage as ReturnType).mockResolvedValueOnce({ ok: false, error: 'remote_unreachable' }); @@ -64,3 +83,109 @@ describe('settings view', () => { expect(status.textContent).toMatch(/remote_unreachable/); }); }); + +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(); + settingsResponses(); + + await renderSettings(app); + + const digitInput = app.querySelector('#display-digit-color'); + const symbolInput = app.querySelector('#display-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' }, + }); + settingsResponses(); + + await renderSettings(app); + + const digitInput = app.querySelector('#display-digit-color'); + const symbolInput = app.querySelector('#display-symbol-color'); + expect(digitInput!.value).toBe('#112233'); + expect(symbolInput!.value).toBe('#aabbcc'); + }); + + it('renders a color-preview-swatch element', async () => { + mockChromeStorage(); + settingsResponses(); + + await renderSettings(app); + + expect(app.querySelector('#display-swatch')).not.toBeNull(); + }); + + it('changing digit color calls saveColorScheme with updated scheme', async () => { + mockChromeStorage(); + settingsResponses(); + + await renderSettings(app); + + const digitInput = app.querySelector('#display-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(); + settingsResponses(); + + await renderSettings(app); + + const symbolInput = app.querySelector('#display-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' }, + }); + settingsResponses(); + + await renderSettings(app); + + const resetBtn = app.querySelector('#display-reset')!; + resetBtn.click(); + 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')!; + expect(digitInput.value).toBe(DEFAULT_DIGIT_COLOR); + expect(symbolInput.value).toBe(DEFAULT_SYMBOL_COLOR); + }); +}); diff --git a/extension/src/popup/components/settings.ts b/extension/src/popup/components/settings.ts index 0942d83..2cea82d 100644 --- a/extension/src/popup/components/settings.ts +++ b/extension/src/popup/components/settings.ts @@ -3,6 +3,11 @@ import { sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { DeviceSettings } from '../../shared/types'; import { GLYPH_TRASH, GLYPH_DEVICES } from '../../shared/glyphs'; +import { + loadColorScheme, saveColorScheme, resetColorScheme, + DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR, +} from '../../shared/color-scheme'; +import { colorizePassword } from '../../shared/password-coloring'; export async function renderSettings(app: HTMLElement): Promise { app.innerHTML = '
'; @@ -62,6 +67,9 @@ export async function renderSettings(app: HTMLElement): Promise {
+
+
+
blacklisted sites
@@ -119,4 +127,65 @@ export async function renderSettings(app: HTMLElement): Promise { } }); }); + + // Render Display section after the rest of the DOM is ready + await renderDisplaySection(); +} + +function updateSwatch(swatch: HTMLElement, digitColor: string, symbolColor: string): void { + swatch.style.setProperty('--relicario-pwd-digit-color', digitColor); + swatch.style.setProperty('--relicario-pwd-symbol-color', symbolColor); + swatch.innerHTML = ''; + swatch.appendChild(colorizePassword('Abc123!@#xyz')); +} + +async function renderDisplaySection(): Promise { + // The Display section container must be present in the DOM before we call this + const container = document.getElementById('display-section-container'); + if (!container) return; + + const scheme = await loadColorScheme(); + + container.innerHTML = ` +
display
+
+ +
+
+ +
+
+
+ +
+ `; + + const digitInput = document.getElementById('display-digit-color') as HTMLInputElement; + const symbolInput = document.getElementById('display-symbol-color') as HTMLInputElement; + const swatch = document.getElementById('display-swatch') as HTMLElement; + + // Render initial swatch + updateSwatch(swatch, scheme.digit_color, scheme.symbol_color); + + async function onColorChange(): Promise { + const newScheme = { digit_color: digitInput.value, symbol_color: symbolInput.value }; + await saveColorScheme(newScheme); + updateSwatch(swatch, newScheme.digit_color, newScheme.symbol_color); + } + + digitInput.addEventListener('change', () => void onColorChange()); + symbolInput.addEventListener('change', () => void onColorChange()); + + document.getElementById('display-reset')?.addEventListener('click', async () => { + await resetColorScheme(); + digitInput.value = DEFAULT_DIGIT_COLOR; + symbolInput.value = DEFAULT_SYMBOL_COLOR; + updateSwatch(swatch, DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR); + }); } diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 0e3a3d1..5379e8e 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1563,3 +1563,13 @@ textarea { .pwd-digit { color: var(--relicario-pwd-digit-color); } .pwd-symbol { color: var(--relicario-pwd-symbol-color); } .pwd-letter { color: inherit; } + +.color-preview-swatch { + font-family: ui-monospace, monospace; + font-size: 1.1rem; + padding: 8px 12px; + border: 1px solid var(--border-mid); + border-radius: 4px; + margin-top: 8px; + background: var(--bg-input); +}