diff --git a/extension/src/popup/components/types/__tests__/sections-save.test.ts b/extension/src/popup/components/types/__tests__/sections-save.test.ts new file mode 100644 index 0000000..8578ea8 --- /dev/null +++ b/extension/src/popup/components/types/__tests__/sections-save.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../popup', async () => { + const navigate = vi.fn(); + const setState = vi.fn(); + const sendMessage = vi.fn(); + const getState = vi.fn(() => ({ + view: 'add', entries: [], selectedId: null, selectedItem: null, selectedIndex: 0, + searchQuery: '', activeGroup: null, error: null, loading: false, + capturedTabId: null, capturedUrl: '', newType: 'login', + vaultSettings: null, generatorDefaults: null, + })); + const escapeHtml = (s: string) => s + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + return { navigate, setState, sendMessage, getState, escapeHtml }; +}); + +import { renderForm } from '../login'; +import { sendMessage } from '../../../popup'; + +describe('Login form packs sectionsDraft into Item.sections', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + vi.mocked(sendMessage).mockReset(); + vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { id: 'fakeid0000000000', items: [] } }); + }); + + it('persists added sections and fields', async () => { + const app = document.getElementById('app')!; + renderForm(app, 'add', null); + + (document.getElementById('f-title') as HTMLInputElement).value = 'Example'; + + (document.querySelector('.disclosure__toggle') as HTMLButtonElement).click(); + (document.querySelector('.add-section') as HTMLButtonElement).click(); + (document.querySelector('[data-add-field="text"]') as HTMLButtonElement).click(); + + const labelInput = document.querySelector('[data-field-label="0-0"]') as HTMLInputElement; + labelInput.value = 'recovery email'; + labelInput.dispatchEvent(new Event('input', { bubbles: true })); + const valueInput = document.querySelector('[data-field-value-input="0-0"]') as HTMLInputElement; + valueInput.value = 'backup@example.com'; + valueInput.dispatchEvent(new Event('input', { bubbles: true })); + + document.getElementById('save-btn')!.click(); + await new Promise((r) => setTimeout(r, 5)); + + const calls = vi.mocked(sendMessage).mock.calls; + const addCall = calls.find(([msg]) => (msg as { type: string }).type === 'add_item'); + const msg = addCall![0] as { type: 'add_item'; item: any }; + expect(msg.item.sections).toHaveLength(1); + expect(msg.item.sections[0].fields).toHaveLength(1); + expect(msg.item.sections[0].fields[0].label).toBe('recovery email'); + expect(msg.item.sections[0].fields[0].value).toEqual({ kind: 'text', value: 'backup@example.com' }); + expect(msg.item.sections[0].fields[0].id).toMatch(/^[0-9a-f]{16}$/); + }); +}); diff --git a/extension/src/popup/components/types/card.ts b/extension/src/popup/components/types/card.ts index d855857..ce5e1fb 100644 --- a/extension/src/popup/components/types/card.ts +++ b/extension/src/popup/components/types/card.ts @@ -2,15 +2,17 @@ /// Detail view has a styled card-silhouette signature block. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup'; -import type { Item, ItemId, ManifestEntry, CardKind } from '../../../shared/types'; +import type { Item, ItemId, ManifestEntry, CardKind, Section } from '../../../shared/types'; import { renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections, + renderSectionsEditor, wireSectionsEditor, } from '../fields'; const CARD_KINDS: CardKind[] = ['credit', 'debit', 'gift', 'loyalty', 'other']; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null; +let sectionsExpanded = false; export function teardown(): void { if (activeKeyHandler) { @@ -21,6 +23,7 @@ export function teardown(): void { document.removeEventListener('keydown', activeFormEscHandler); activeFormEscHandler = null; } + sectionsExpanded = false; } function brandFromNumber(num: string): string { @@ -140,6 +143,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite const c = (existing?.core.type === 'card') ? existing.core : null; const currentYear = new Date().getFullYear(); + const sectionsDraft: Section[] = existing + ? JSON.parse(JSON.stringify(existing.sections)) as Section[] + : []; + const monthOptions = Array.from({ length: 12 }, (_, i) => { const m = String(i + 1).padStart(2, '0'); const sel = c?.expiry?.month === i + 1 ? 'selected' : ''; @@ -176,6 +183,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite