test(ext/vault): vitest for the Import panel

Mocks sendMessage. Covers: file-picker fires
parse_lastpass_csv, preview text matches the parsed counts,
confirm fires import_lastpass_commit with the parsed items,
warnings render after import, cancel clears the preview.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-30 18:45:23 -04:00
parent 66981588e7
commit ab5a885f10

View File

@@ -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<typeof vi.fn>;
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<void> {
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 = '<div id="app"></div>';
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);
});
});