feat(ext/settings): Display section with color pickers + swatch + reset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-02 19:03:23 -04:00
parent b928ed407b
commit b2fc56709a
3 changed files with 204 additions and 0 deletions

View File

@@ -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<string, unknown> = {}) {
const store: Record<string, unknown> = { ...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<string, unknown>) => { 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<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
@@ -52,6 +70,7 @@ describe('settings view', () => {
});
it('shows the error when sync fails', async () => {
mockChromeStorage();
settingsResponses();
(sendMessage as ReturnType<typeof vi.fn>).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 = '<div id="app"></div>';
app = document.getElementById('app')!;
(sendMessage as ReturnType<typeof vi.fn>).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<HTMLInputElement>('#display-digit-color');
const symbolInput = app.querySelector<HTMLInputElement>('#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<HTMLInputElement>('#display-digit-color');
const symbolInput = app.querySelector<HTMLInputElement>('#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<HTMLInputElement>('#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<typeof vi.fn>;
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<HTMLInputElement>('#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<typeof vi.fn>;
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<HTMLButtonElement>('#display-reset')!;
resetBtn.click();
await new Promise((r) => setTimeout(r, 0));
const syncRemove = (global as any).chrome.storage.sync.remove as ReturnType<typeof vi.fn>;
expect(syncRemove).toHaveBeenCalledWith('password_display_scheme');
const digitInput = app.querySelector<HTMLInputElement>('#display-digit-color')!;
const symbolInput = app.querySelector<HTMLInputElement>('#display-symbol-color')!;
expect(digitInput.value).toBe(DEFAULT_DIGIT_COLOR);
expect(symbolInput.value).toBe(DEFAULT_SYMBOL_COLOR);
});
});