diff --git a/extension/src/vault/components/__tests__/import-panel.test.ts b/extension/src/vault/components/__tests__/import-panel.test.ts new file mode 100644 index 0000000..3b90ad4 --- /dev/null +++ b/extension/src/vault/components/__tests__/import-panel.test.ts @@ -0,0 +1,152 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +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 { renderImportPanel, teardown } from '../import-panel'; + +const mockSendMessage = sendMessage as ReturnType; + +function fakeItem(type: 'login' | 'secure_note', title: string) { + return { + id: 'fake0000', + title, + type, + tags: [], + favorite: false, + created: 0, + modified: 0, + core: type === 'login' + ? { type: 'login', username: 'u', password: 'p' } + : { type: 'secure_note', body: 'note' }, + sections: [], + attachments: [], + field_history: {}, + }; +} + +async function pickFile(app: HTMLElement, body: string): Promise { + const file = new File([body], 'export.csv', { type: 'text/csv' }); + const input = app.querySelector('#lp-file') as HTMLInputElement; + Object.defineProperty(input, 'files', { value: [file] }); + input.dispatchEvent(new Event('change')); + // Allow microtasks to drain. + await new Promise((r) => setTimeout(r, 10)); +} + +describe('Import panel', () => { + let app: HTMLElement; + + beforeEach(() => { + mockSendMessage.mockReset(); + teardown(); + document.body.innerHTML = '
'; + app = document.getElementById('app')!; + }); + + it('parsing fires parse_lastpass_csv with the file bytes', async () => { + renderImportPanel(app); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { items: [fakeItem('login', 'A')], warnings: [] }, + }); + + await pickFile(app, 'url,username,password,totp,extra,name,grouping,fav\nhttps://x,u,p,,,A,,'); + + expect(mockSendMessage).toHaveBeenCalledTimes(1); + expect((mockSendMessage.mock.calls[0][0] as { type: string }).type).toBe('parse_lastpass_csv'); + }); + + it('preview text shows parsed counts', async () => { + renderImportPanel(app); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { + items: [fakeItem('login', 'A'), fakeItem('login', 'B'), fakeItem('secure_note', 'N')], + warnings: [{ row: 5, title: 'X', message: 'missing `password` — skipped' }], + }, + }); + + await pickFile(app, 'header,row,doesnt,matter,for,this,test,case'); + + const txt = (app.querySelector('#lp-preview-text') as HTMLElement).textContent ?? ''; + expect(txt).toContain('2 logins'); + expect(txt).toContain('1 notes'); + expect(txt).toContain('1 skipped'); + expect(txt).toContain('proceed?'); + }); + + it('confirm fires import_lastpass_commit with the parsed items', async () => { + renderImportPanel(app); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { items: [fakeItem('login', 'A'), fakeItem('login', 'B')], warnings: [] }, + }); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { summary: { itemCount: 2 } }, + }); + + await pickFile(app, 'header'); + (app.querySelector('#lp-confirm-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 10)); + + expect(mockSendMessage).toHaveBeenCalledTimes(2); + const second = mockSendMessage.mock.calls[1][0] as { + type: string; + items: unknown[]; + }; + expect(second.type).toBe('import_lastpass_commit'); + expect(second.items).toHaveLength(2); + }); + + it('renders warning list after a successful import', async () => { + renderImportPanel(app); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { + items: [fakeItem('login', 'A')], + warnings: [ + { row: 4, title: 'AWS', message: 'invalid base32 TOTP secret — login imported without TOTP' }, + ], + }, + }); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { summary: { itemCount: 1 } }, + }); + + await pickFile(app, 'header'); + (app.querySelector('#lp-confirm-btn') as HTMLButtonElement).click(); + await new Promise((r) => setTimeout(r, 10)); + + const list = app.querySelector('#lp-warning-list')?.textContent ?? ''; + expect(list).toContain('row 4'); + expect(list).toContain('AWS'); + expect(list).toContain('TOTP'); + }); + + it('cancel clears the preview', async () => { + renderImportPanel(app); + mockSendMessage.mockResolvedValueOnce({ + ok: true, + data: { items: [fakeItem('login', 'A')], warnings: [] }, + }); + + await pickFile(app, 'header'); + expect(app.querySelector('#lp-preview')!.classList.contains('hidden')).toBe(false); + + (app.querySelector('#lp-cancel-btn') as HTMLButtonElement).click(); + expect(app.querySelector('#lp-preview')!.classList.contains('hidden')).toBe(true); + }); +});