diff --git a/extension/src/popup/components/__tests__/devices.test.ts b/extension/src/popup/components/__tests__/devices.test.ts index 7e73520..60c31b7 100644 --- a/extension/src/popup/components/__tests__/devices.test.ts +++ b/extension/src/popup/components/__tests__/devices.test.ts @@ -33,11 +33,16 @@ describe('devices view', () => { vi.clearAllMocks(); }); + // The component fires list_devices + list_revoked in parallel via Promise.all, + // so every render needs both mocked. Helper makes the per-test setup readable. + function mockListPair(devices: unknown[], revoked: unknown[] = []): void { + (sendMessage as ReturnType) + .mockResolvedValueOnce({ ok: true, data: { devices } }) + .mockResolvedValueOnce({ ok: true, data: { revoked } }); + } + it('renders empty state when no devices', async () => { - (sendMessage as ReturnType).mockResolvedValueOnce({ - ok: true, - data: { devices: [] }, - }); + mockListPair([]); await renderDevices(app); @@ -45,15 +50,10 @@ describe('devices view', () => { }); it('renders devices with "you" indicator on current device', async () => { - (sendMessage as ReturnType).mockResolvedValueOnce({ - ok: true, - data: { - devices: [ - { name: 'Chrome on Linux', public_key: 'abc', added_at: 1000 }, - { name: 'CLI', public_key: 'def', added_at: 500 }, - ], - }, - }); + mockListPair([ + { name: 'Chrome on Linux', public_key: 'abc', added_at: 1000 }, + { name: 'CLI', public_key: 'def', added_at: 500 }, + ]); await renderDevices(app); @@ -68,23 +68,15 @@ describe('devices view', () => { it('shows unregistered banner when current device not in list', async () => { (chrome.storage.local.get as ReturnType).mockResolvedValueOnce({ device_name: 'Unknown' }); - (sendMessage as ReturnType).mockResolvedValueOnce({ - ok: true, - data: { - devices: [{ name: 'CLI', public_key: 'abc', added_at: 1000 }], - }, - }); + mockListPair([{ name: 'CLI', public_key: 'abc', added_at: 1000 }]); await renderDevices(app); - expect(app.innerHTML).toContain('This device is not registered'); + expect(app.innerHTML).toContain("This device isn't registered"); }); it('back button navigates to list', async () => { - (sendMessage as ReturnType).mockResolvedValueOnce({ - ok: true, - data: { devices: [] }, - }); + mockListPair([]); await renderDevices(app); app.querySelector('#back-btn')?.click(); @@ -94,10 +86,7 @@ describe('devices view', () => { it('clicking register button reveals an inline name input', async () => { (chrome.storage.local.get as ReturnType).mockResolvedValueOnce({ device_name: 'Unknown' }); - (sendMessage as ReturnType).mockResolvedValueOnce({ - ok: true, - data: { devices: [{ name: 'CLI', public_key: 'k', added_at: 1 }] }, - }); + mockListPair([{ name: 'CLI', public_key: 'k', added_at: 1 }]); await renderDevices(app); app.querySelector('#register-btn')!.click(); @@ -108,13 +97,15 @@ describe('devices view', () => { it('confirming register sends register_this_device with the entered name', async () => { (chrome.storage.local.get as ReturnType).mockResolvedValueOnce({ device_name: 'Unknown' }); - // Initial list_devices. - (sendMessage as ReturnType) - .mockResolvedValueOnce({ ok: true, data: { devices: [{ name: 'CLI', public_key: 'k', added_at: 1 }] } }) - // register_this_device. - .mockResolvedValueOnce({ ok: true }) - // Re-render's list_devices. - .mockResolvedValueOnce({ ok: true, data: { devices: [{ name: 'CLI', public_key: 'k', added_at: 1 }, { name: 'Test Browser', public_key: 'q', added_at: 2 }] } }); + // Initial render: list_devices + list_revoked. + mockListPair([{ name: 'CLI', public_key: 'k', added_at: 1 }]); + // register_this_device. + (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true }); + // Re-render: list_devices + list_revoked. + mockListPair([ + { name: 'CLI', public_key: 'k', added_at: 1 }, + { name: 'Test Browser', public_key: 'q', added_at: 2 }, + ]); // Re-render also re-reads device_name from storage. (chrome.storage.local.get as ReturnType).mockResolvedValueOnce({ device_name: 'Test Browser' }); diff --git a/extension/src/service-worker/__tests__/devices.test.ts b/extension/src/service-worker/__tests__/devices.test.ts index 5384f95..bee353d 100644 --- a/extension/src/service-worker/__tests__/devices.test.ts +++ b/extension/src/service-worker/__tests__/devices.test.ts @@ -3,10 +3,19 @@ import { readDevices, addDevice, revokeDevice } from '../devices'; import type { GitHost } from '../git-host'; function makeGitHost(devicesJson = '{"devices":[]}'): GitHost { - let stored = devicesJson; + // Per-path storage — revokeDevice writes devices.json AND revoked.json, + // so a single slot would corrupt the second read. + const files = new Map(); + files.set('.relicario/devices.json', devicesJson); return { - readFile: vi.fn().mockImplementation(async () => new TextEncoder().encode(stored)), - writeFile: vi.fn().mockImplementation(async (_p, bytes) => { stored = new TextDecoder().decode(bytes); }), + readFile: vi.fn().mockImplementation(async (path: string) => { + const content = files.get(path); + if (content === undefined) throw new Error(`404: ${path}`); + return new TextEncoder().encode(content); + }), + writeFile: vi.fn().mockImplementation(async (path: string, bytes: Uint8Array) => { + files.set(path, new TextDecoder().decode(bytes)); + }), deleteFile: vi.fn(), listDir: vi.fn(), putBlob: vi.fn(),