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' });
});
});