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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
app.innerHTML = '<div class="pad" style="text-align:center; padding-top:20px;"><span class="spinner"></span></div>';
|
||||
@@ -62,6 +67,9 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||
<div id="sync-status" class="muted" style="font-size:12px;min-height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px;" id="display-section-container">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-size:12px; color:#8b949e; margin-bottom:6px;">blacklisted sites</div>
|
||||
<div id="blacklist-container">
|
||||
@@ -119,4 +127,65 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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<void> {
|
||||
// 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 = `
|
||||
<div style="font-size:12px; color:#8b949e; margin-bottom:6px;">display</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:13px;">
|
||||
<input type="color" id="display-digit-color" value="${escapeHtml(scheme.digit_color)}">
|
||||
digit color
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:13px;">
|
||||
<input type="color" id="display-symbol-color" value="${escapeHtml(scheme.symbol_color)}">
|
||||
symbol color
|
||||
</label>
|
||||
</div>
|
||||
<div id="display-swatch" class="color-preview-swatch"></div>
|
||||
<div style="margin-top:8px;">
|
||||
<button id="display-reset" class="btn" style="font-size:11px;">reset to defaults</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user