diff --git a/extension/src/vault/components/__tests__/backup-panel.test.ts b/extension/src/vault/components/__tests__/backup-panel.test.ts new file mode 100644 index 0000000..5369126 --- /dev/null +++ b/extension/src/vault/components/__tests__/backup-panel.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// backup-panel.ts only imports { sendMessage } from '../../shared/state'. +// We provide the full shared/state surface so TypeScript is satisfied. +vi.mock('../../../shared/state', () => ({ + sendMessage: vi.fn(), + openVaultTab: vi.fn(), + registerHost: vi.fn(), + getState: vi.fn(), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: vi.fn(() => false), +})); + +import { sendMessage } from '../../../shared/state'; +import { renderBackupPanel, teardown } from '../backup-panel'; + +const mockSendMessage = sendMessage as ReturnType; + +describe('backup panel — export', () => { + let app: HTMLElement; + + beforeEach(() => { + mockSendMessage.mockReset(); + teardown(); + document.body.innerHTML = '
'; + app = document.getElementById('app')!; + }); + + it('clicking export with no passphrase is a no-op', async () => { + renderBackupPanel(app); + vi.spyOn(window, 'prompt').mockReturnValue(null); + + (app.querySelector('#export-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 0)); + + expect(mockSendMessage).not.toHaveBeenCalled(); + }); + + it('clicking export with a passphrase fires export_backup', async () => { + renderBackupPanel(app); + vi.spyOn(window, 'prompt').mockReturnValue('test-backup-pass'); + mockSendMessage.mockResolvedValue({ ok: true, data: { bytes: new ArrayBuffer(123) } }); + + (app.querySelector('#include-image') as HTMLInputElement).checked = true; + (app.querySelector('#export-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 50)); + + expect(mockSendMessage).toHaveBeenCalledWith({ + type: 'export_backup', + passphrase: 'test-backup-pass', + includeImage: true, + }); + }); + + it('export error surfaces in the status pre', async () => { + renderBackupPanel(app); + vi.spyOn(window, 'prompt').mockReturnValue('p'); + mockSendMessage.mockResolvedValue({ ok: false, error: 'no reference image stored locally' }); + + (app.querySelector('#export-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 50)); + + const status = app.querySelector('#export-status') as HTMLElement; + expect(status.textContent).toContain('Failed'); + expect(status.textContent).toContain('no reference image'); + }); +}); + +describe('backup panel — restore', () => { + let app: HTMLElement; + + beforeEach(() => { + mockSendMessage.mockReset(); + teardown(); + document.body.innerHTML = '
'; + app = document.getElementById('app')!; + }); + + it('clicking restore without filling new-remote shows an error', async () => { + renderBackupPanel(app); + vi.spyOn(window, 'prompt').mockReturnValue('p'); + + // Simulate file picked — this reveals the fieldset with the restore button. + const fakeFile = new File([new Uint8Array([0x52, 0x42, 0x41, 0x4b, 0x01])], 'v.relbak'); + const input = app.querySelector('#restore-file') as HTMLInputElement; + Object.defineProperty(input, 'files', { value: [fakeFile] }); + input.dispatchEvent(new Event('change')); + + (app.querySelector('#restore-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 50)); + + const status = app.querySelector('#restore-status') as HTMLElement; + expect(status.textContent).toContain('fill in'); + expect(mockSendMessage).not.toHaveBeenCalled(); + }); +});