feat(ext/popup): devices view — list devices with revoke actions

Shows registered devices with "← you" indicator on current device.
Revoke button on other devices. Unregistered banner if current
device not in list.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-26 19:34:30 -04:00
parent 9fbf9bb3ee
commit 7fe54472b3
3 changed files with 255 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
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('../../popup', () => ({
setState: vi.fn(),
sendMessage: vi.fn(),
navigate: vi.fn(),
escapeHtml: (s: string) => s,
}));
import { sendMessage, navigate } from '../../popup';
describe('devices view', () => {
let app: HTMLElement;
beforeEach(() => {
document.body.innerHTML = '<div id="app"></div>';
app = document.getElementById('app')!;
vi.clearAllMocks();
});
it('renders empty state when no devices', async () => {
(sendMessage as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValueOnce({ device_name: 'Unknown' });
(sendMessage as ReturnType<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
data: { devices: [] },
});
await renderDevices(app);
app.querySelector<HTMLButtonElement>('#back-btn')?.click();
expect(navigate).toHaveBeenCalledWith('list');
});
});