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