feat(ext/popup): rewrite generator as inline panel with ✨ trigger
The popover (which clipped off the popup edge) becomes an inline panel that mounts inside the form (login.ts) or settings section (settings-vault.ts). Trigger button is ✨ with aria-expanded toggling. Action row varies by context: fill-field has cancel+use; configure- defaults has only the save-default link. Escape key closes the panel. Tests adapted to new API; 3 new tests for aria-expanded, auto-generate, and Escape behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ vi.mock('../../popup', async () => {
|
||||
return { sendMessage };
|
||||
});
|
||||
|
||||
import { openGeneratorPopover, closeGeneratorPopover } from '../generator-panel';
|
||||
import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel';
|
||||
import { sendMessage } from '../../popup';
|
||||
import type { GeneratorRequest } from '../../../shared/types';
|
||||
|
||||
@@ -16,28 +16,35 @@ const DEFAULT_REQ: GeneratorRequest = {
|
||||
symbol_charset: { kind: 'safe_only' },
|
||||
};
|
||||
|
||||
function setupAnchor(): HTMLElement {
|
||||
document.body.innerHTML = '<button id="anchor">gen</button>';
|
||||
return document.getElementById('anchor')!;
|
||||
function setupMount(): { parent: HTMLElement; trigger: HTMLElement } {
|
||||
document.body.innerHTML = `
|
||||
<div id="parent">
|
||||
<button id="trigger" aria-expanded="false">✨</button>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
parent: document.getElementById('parent')!,
|
||||
trigger: document.getElementById('trigger')!,
|
||||
};
|
||||
}
|
||||
|
||||
describe('generator-popover', () => {
|
||||
describe('generator-panel', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(sendMessage).mockReset();
|
||||
vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { password: 'Kj7%pW@2xNq!8rMvT' } });
|
||||
});
|
||||
|
||||
it('opens a popover with Random kind by default', async () => {
|
||||
const anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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('.generator-popover')).not.toBeNull();
|
||||
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 anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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';
|
||||
@@ -54,8 +61,8 @@ describe('generator-popover', () => {
|
||||
});
|
||||
|
||||
it('BIP39 toggle swaps to generate_passphrase', async () => {
|
||||
const anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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));
|
||||
@@ -64,13 +71,13 @@ describe('generator-popover', () => {
|
||||
});
|
||||
|
||||
it('use-this-value invokes onPicked with current preview and closes', async () => {
|
||||
const anchor = setupAnchor();
|
||||
const { parent, trigger } = setupMount();
|
||||
const onPicked = vi.fn();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked });
|
||||
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('.generator-popover')).toBeNull();
|
||||
expect(document.querySelector('.gen-panel')).toBeNull();
|
||||
});
|
||||
|
||||
it('save-as-default sends update_vault_settings with the current request', async () => {
|
||||
@@ -88,8 +95,8 @@ describe('generator-popover', () => {
|
||||
if (msg.type === 'update_vault_settings') return { ok: true };
|
||||
return { ok: false, error: 'unhandled' };
|
||||
});
|
||||
const anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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));
|
||||
@@ -102,8 +109,8 @@ describe('generator-popover', () => {
|
||||
});
|
||||
|
||||
it('disables use-button when no char class selected (Random)', async () => {
|
||||
const anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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;
|
||||
@@ -114,11 +121,40 @@ describe('generator-popover', () => {
|
||||
expect(useBtn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('closeGeneratorPopover removes the DOM + handlers', async () => {
|
||||
const anchor = setupAnchor();
|
||||
openGeneratorPopover({ anchor, initial: DEFAULT_REQ, onPicked: vi.fn() });
|
||||
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));
|
||||
closeGeneratorPopover();
|
||||
expect(document.querySelector('.generator-popover')).toBeNull();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user