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:
152
extension/src/vault/components/__tests__/import-panel.test.ts
Normal file
152
extension/src/vault/components/__tests__/import-panel.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user