feat(ext/shared): color-scheme storage + applyColorScheme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
76
extension/src/shared/__tests__/color-scheme.test.ts
Normal file
76
extension/src/shared/__tests__/color-scheme.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
loadColorScheme, saveColorScheme, resetColorScheme, applyColorScheme,
|
||||||
|
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
|
||||||
|
} from '../color-scheme';
|
||||||
|
|
||||||
|
function mockChromeStorage(initial: any = {}) {
|
||||||
|
const store = { ...initial };
|
||||||
|
(global as any).chrome = {
|
||||||
|
storage: {
|
||||||
|
sync: {
|
||||||
|
get: vi.fn((key: string) => Promise.resolve(
|
||||||
|
key in store ? { [key]: store[key] } : {})),
|
||||||
|
set: vi.fn((kv: any) => { Object.assign(store, kv); return Promise.resolve(); }),
|
||||||
|
remove: vi.fn((key: string) => { delete store[key]; return Promise.resolve(); }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('color-scheme storage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// happy-dom provides document globally; reset inline styles between tests
|
||||||
|
document.documentElement.removeAttribute('style');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load returns defaults when storage is empty', async () => {
|
||||||
|
mockChromeStorage();
|
||||||
|
const scheme = await loadColorScheme();
|
||||||
|
expect(scheme.digit_color).toBe(DEFAULT_DIGIT_COLOR);
|
||||||
|
expect(scheme.symbol_color).toBe(DEFAULT_SYMBOL_COLOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load returns stored values when present', async () => {
|
||||||
|
mockChromeStorage({
|
||||||
|
password_display_scheme: { digit_color: '#123456', symbol_color: '#abcdef' },
|
||||||
|
});
|
||||||
|
const scheme = await loadColorScheme();
|
||||||
|
expect(scheme.digit_color).toBe('#123456');
|
||||||
|
expect(scheme.symbol_color).toBe('#abcdef');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save round-trips', async () => {
|
||||||
|
mockChromeStorage();
|
||||||
|
await saveColorScheme({ digit_color: '#111111', symbol_color: '#222222' });
|
||||||
|
const scheme = await loadColorScheme();
|
||||||
|
expect(scheme).toEqual({ digit_color: '#111111', symbol_color: '#222222' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reset removes the storage key', async () => {
|
||||||
|
const store = mockChromeStorage({
|
||||||
|
password_display_scheme: { digit_color: '#000000', symbol_color: '#ffffff' },
|
||||||
|
});
|
||||||
|
await resetColorScheme();
|
||||||
|
expect(store.password_display_scheme).toBeUndefined();
|
||||||
|
const scheme = await loadColorScheme();
|
||||||
|
expect(scheme.digit_color).toBe(DEFAULT_DIGIT_COLOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('apply sets CSS custom properties on document.documentElement', async () => {
|
||||||
|
mockChromeStorage({
|
||||||
|
password_display_scheme: { digit_color: '#deadbe', symbol_color: '#feed00' },
|
||||||
|
});
|
||||||
|
await applyColorScheme();
|
||||||
|
const root = document.documentElement.style;
|
||||||
|
expect(root.getPropertyValue('--relicario-pwd-digit-color').trim()).toBe('#deadbe');
|
||||||
|
expect(root.getPropertyValue('--relicario-pwd-symbol-color').trim()).toBe('#feed00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save rejects malformed hex values', async () => {
|
||||||
|
mockChromeStorage();
|
||||||
|
await expect(saveColorScheme({ digit_color: 'not-a-color', symbol_color: '#ffffff' }))
|
||||||
|
.rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
48
extension/src/shared/color-scheme.ts
Normal file
48
extension/src/shared/color-scheme.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export const DEFAULT_DIGIT_COLOR = '#2563eb';
|
||||||
|
export const DEFAULT_SYMBOL_COLOR = '#dc2626';
|
||||||
|
const STORAGE_KEY = 'password_display_scheme';
|
||||||
|
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
|
export interface ColorScheme {
|
||||||
|
digit_color: string;
|
||||||
|
symbol_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SCHEME: ColorScheme = {
|
||||||
|
digit_color: DEFAULT_DIGIT_COLOR,
|
||||||
|
symbol_color: DEFAULT_SYMBOL_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValid(s: ColorScheme): boolean {
|
||||||
|
return HEX_RE.test(s.digit_color) && HEX_RE.test(s.symbol_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadColorScheme(): Promise<ColorScheme> {
|
||||||
|
const result = await chrome.storage.sync.get(STORAGE_KEY);
|
||||||
|
const stored = result[STORAGE_KEY] as Partial<ColorScheme> | undefined;
|
||||||
|
if (!stored) return { ...DEFAULT_SCHEME };
|
||||||
|
return {
|
||||||
|
digit_color: typeof stored.digit_color === 'string' && HEX_RE.test(stored.digit_color)
|
||||||
|
? stored.digit_color : DEFAULT_DIGIT_COLOR,
|
||||||
|
symbol_color: typeof stored.symbol_color === 'string' && HEX_RE.test(stored.symbol_color)
|
||||||
|
? stored.symbol_color : DEFAULT_SYMBOL_COLOR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveColorScheme(scheme: ColorScheme): Promise<void> {
|
||||||
|
if (!isValid(scheme)) {
|
||||||
|
throw new Error('Invalid color values; expected #rrggbb hex strings.');
|
||||||
|
}
|
||||||
|
await chrome.storage.sync.set({ [STORAGE_KEY]: scheme });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetColorScheme(): Promise<void> {
|
||||||
|
await chrome.storage.sync.remove(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyColorScheme(): Promise<void> {
|
||||||
|
const scheme = await loadColorScheme();
|
||||||
|
const root = document.documentElement.style;
|
||||||
|
root.setProperty('--relicario-pwd-digit-color', scheme.digit_color);
|
||||||
|
root.setProperty('--relicario-pwd-symbol-color', scheme.symbol_color);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user