Tests were written against the pre-Stream B flat settings page. After
the left-nav restructure (bd6a301) and the management-surfaces revamp,
the Display section's IDs are only in the DOM once the user navigates
there, and renderSettings makes additional sendMessage calls (is_unlocked,
per-section data) that the original mocks didn't cover.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
219 lines
7.6 KiB
TypeScript
219 lines
7.6 KiB
TypeScript
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<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(); }),
|
|
},
|
|
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<typeof vi.fn>)
|
|
.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<void> {
|
|
const btn = app.querySelector<HTMLButtonElement>('[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 = '<div id="app"></div>';
|
|
app = document.getElementById('app')!;
|
|
(sendMessage as ReturnType<typeof vi.fn>).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<HTMLInputElement>('#capture-enabled')).not.toBeNull();
|
|
});
|
|
|
|
it('toggling capture-enabled sends an update_settings message', async () => {
|
|
mockChromeStorage();
|
|
mockDefaultLanding();
|
|
(sendMessage as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
|
|
|
|
await renderSettings(app);
|
|
const cb = app.querySelector<HTMLInputElement>('#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 = '<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();
|
|
mockDefaultLanding();
|
|
|
|
await renderSettings(app);
|
|
await navigateToDisplay(app);
|
|
|
|
const digitInput = app.querySelector<HTMLInputElement>('#digit-color');
|
|
const symbolInput = app.querySelector<HTMLInputElement>('#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<HTMLInputElement>('#digit-color');
|
|
const symbolInput = app.querySelector<HTMLInputElement>('#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<HTMLInputElement>('#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();
|
|
mockDefaultLanding();
|
|
|
|
await renderSettings(app);
|
|
await navigateToDisplay(app);
|
|
|
|
const symbolInput = app.querySelector<HTMLInputElement>('#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' },
|
|
});
|
|
mockDefaultLanding();
|
|
|
|
await renderSettings(app);
|
|
await navigateToDisplay(app);
|
|
|
|
const resetBtn = app.querySelector<HTMLButtonElement>('#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<typeof vi.fn>;
|
|
expect(syncRemove).toHaveBeenCalledWith('password_display_scheme');
|
|
|
|
const digitInput = app.querySelector<HTMLInputElement>('#digit-color')!;
|
|
const symbolInput = app.querySelector<HTMLInputElement>('#symbol-color')!;
|
|
expect(digitInput.value).toBe(DEFAULT_DIGIT_COLOR);
|
|
expect(symbolInput.value).toBe(DEFAULT_SYMBOL_COLOR);
|
|
});
|
|
});
|