import { beforeEach, describe, expect, it, vi } from 'vitest'; import { renderDevices } from '../devices'; // Mock chrome.storage.local // @ts-expect-error test harness globalThis.chrome = { storage: { local: { get: vi.fn().mockResolvedValue({ device_name: 'Chrome on Linux' }), }, }, }; // Mock popup module 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, navigate } from '../../../shared/state'; describe('devices view', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '
'; app = document.getElementById('app')!; vi.clearAllMocks(); }); it('renders empty state when no devices', async () => { (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true, data: { devices: [] }, }); await renderDevices(app); expect(app.innerHTML).toContain('No devices registered'); }); 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 }, ], }, }); await renderDevices(app); expect(app.innerHTML).toContain('Chrome on Linux'); expect(app.innerHTML).toContain('← you'); expect(app.innerHTML).toContain('CLI'); // Current device should not have revoke button const rows = app.querySelectorAll('.device-row'); expect(rows[0].querySelector('[data-revoke]')).toBeNull(); expect(rows[1].querySelector('[data-revoke]')).not.toBeNull(); }); 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 }], }, }); await renderDevices(app); expect(app.innerHTML).toContain('This device is not registered'); }); it('back button navigates to list', async () => { (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true, data: { devices: [] }, }); await renderDevices(app); app.querySelector('#back-btn')?.click(); expect(navigate).toHaveBeenCalledWith('list'); }); 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 }] }, }); await renderDevices(app); app.querySelector('#register-btn')!.click(); expect(app.querySelector('#register-name-input')).not.toBeNull(); expect(app.querySelector('#register-confirm-btn')).not.toBeNull(); }); 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 }] } }); // Re-render also re-reads device_name from storage. (chrome.storage.local.get as ReturnType).mockResolvedValueOnce({ device_name: 'Test Browser' }); await renderDevices(app); app.querySelector('#register-btn')!.click(); const input = app.querySelector('#register-name-input')!; input.value = 'Test Browser'; app.querySelector('#register-confirm-btn')!.click(); // Wait a microtask for the async handler to run. await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0)); expect(sendMessage).toHaveBeenCalledWith({ type: 'register_this_device', name: 'Test Browser' }); }); });