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:
91
extension/src/popup/components/__tests__/devices.test.ts
Normal file
91
extension/src/popup/components/__tests__/devices.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user