import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../../../shared/state', async () => { const sendMessage = vi.fn(); return { sendMessage, popOutToTab: vi.fn(), isInTab: vi.fn(() => false), openVaultTab: vi.fn() }; }); import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel'; import { sendMessage } from '../../../shared/state'; import type { GeneratorRequest } from '../../../shared/types'; const DEFAULT_REQ: GeneratorRequest = { kind: 'random', length: 20, classes: { lower: true, upper: true, digits: true, symbols: true }, symbol_charset: { kind: 'safe_only' }, }; function setupMount(): { parent: HTMLElement; trigger: HTMLElement } { document.body.innerHTML = `
`; return { parent: document.getElementById('parent')!, trigger: document.getElementById('trigger')!, }; } describe('generator-panel', () => { beforeEach(() => { vi.mocked(sendMessage).mockReset(); vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { password: 'Kj7%pW@2xNq!8rMvT' } }); }); it('opens a panel with Random kind by default', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); expect(document.querySelector('.gen-panel')).not.toBeNull(); expect(document.querySelector('#gen-kind-random')?.classList.contains('active')).toBe(true); }); it('sends generate_password on knob change (debounced)', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); const slider = document.querySelector('#gen-length') as HTMLInputElement; slider.value = '32'; slider.dispatchEvent(new Event('input', { bubbles: true })); await new Promise((r) => setTimeout(r, 200)); const calls = vi.mocked(sendMessage).mock.calls.filter( ([msg]) => (msg as { type: string }).type === 'generate_password', ); const latest = calls[calls.length - 1]![0] as { request: GeneratorRequest }; expect(latest.request.kind).toBe('random'); if (latest.request.kind === 'random') { expect(latest.request.length).toBe(32); } }); it('BIP39 toggle swaps to generate_passphrase', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); (document.getElementById('gen-kind-bip39') as HTMLButtonElement).click(); await new Promise((r) => setTimeout(r, 200)); const calls = vi.mocked(sendMessage).mock.calls; expect(calls.some(([msg]) => (msg as { type: string }).type === 'generate_passphrase')).toBe(true); }); it('use-this-value invokes onPicked with current preview and closes', async () => { const { parent, trigger } = setupMount(); const onPicked = vi.fn(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked }); await new Promise((r) => setTimeout(r, 200)); (document.querySelector('#gen-use') as HTMLButtonElement).click(); expect(onPicked).toHaveBeenCalledWith('Kj7%pW@2xNq!8rMvT'); expect(document.querySelector('.gen-panel')).toBeNull(); }); it('save-as-default sends update_vault_settings with the current request', async () => { vi.mocked(sendMessage).mockImplementation(async (msg: any) => { if (msg.type === 'generate_password') return { ok: true, data: { password: 'abc' } }; if (msg.type === 'get_vault_settings') { return { ok: true, data: { settings: { trash_retention: { kind: 'days', value: 30 }, field_history_retention: { kind: 'forever' }, generator_defaults: DEFAULT_REQ, attachment_caps: {}, autofill_origin_acks: {}, } } }; } if (msg.type === 'update_vault_settings') return { ok: true }; return { ok: false, error: 'unhandled' }; }); const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); (document.querySelector('#gen-save-default') as HTMLButtonElement).click(); await new Promise((r) => setTimeout(r, 50)); const updateCall = vi.mocked(sendMessage).mock.calls.find( ([m]) => (m as any).type === 'update_vault_settings', ); expect(updateCall).toBeDefined(); const msg = updateCall![0] as { settings: { generator_defaults: GeneratorRequest } }; expect(msg.settings.generator_defaults.kind).toBe('random'); }); it('disables use-button when no char class selected (Random)', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); for (const id of ['gen-lower', 'gen-upper', 'gen-digits', 'gen-symbols']) { const cb = document.getElementById(id) as HTMLInputElement; cb.checked = false; cb.dispatchEvent(new Event('change', { bubbles: true })); } const useBtn = document.querySelector('#gen-use') as HTMLButtonElement; expect(useBtn.disabled).toBe(true); }); it('closeGeneratorPanel removes the DOM + handlers', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); closeGeneratorPanel(); expect(document.querySelector('.gen-panel')).toBeNull(); }); it('sets aria-expanded on the trigger when opened', async () => { const { parent, trigger } = setupMount(); expect(trigger.getAttribute('aria-expanded')).toBe('false'); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); expect(trigger.getAttribute('aria-expanded')).toBe('true'); closeGeneratorPanel(); expect(trigger.getAttribute('aria-expanded')).toBe('false'); }); it('auto-generates a preview on open', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 200)); const calls = vi.mocked(sendMessage).mock.calls.filter( ([msg]) => (msg as { type: string }).type === 'generate_password', ); expect(calls.length).toBeGreaterThan(0); }); it('Escape key closes the panel', async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); await new Promise((r) => setTimeout(r, 50)); expect(isGeneratorPanelOpen()).toBe(true); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); expect(isGeneratorPanelOpen()).toBe(false); expect(document.querySelector('.gen-panel')).toBeNull(); }); it("configure-defaults context renders only the save-default action (no use/cancel)", async () => { const { parent, trigger } = setupMount(); openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'configure-defaults' }); await new Promise((r) => setTimeout(r, 50)); expect(document.querySelector('#gen-save-default')).not.toBeNull(); expect(document.querySelector('#gen-use')).toBeNull(); expect(document.querySelector('#gen-cancel')).toBeNull(); }); });