diff --git a/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts new file mode 100644 index 0000000..da6b0ed --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireGroupAutocomplete } from '../group-autocomplete'; + +describe('wireGroupAutocomplete', () => { + let form: HTMLElement; + + beforeEach(() => { + // Clean up any datalist from a prior test + document.getElementById('groups-datalist')?.remove(); + form = document.createElement('div'); + form.innerHTML = ``; + document.body.appendChild(form); + }); + + it('attaches datalist with all groups', async () => { + const sendMessage = vi.fn().mockResolvedValue({ + ok: true, + data: { groups: ['personal', 'work', 'finance'] }, + }); + await wireGroupAutocomplete(form, { sendMessage }); + const list = document.getElementById('groups-datalist') as HTMLDataListElement | null; + expect(list).not.toBeNull(); + const opts = Array.from(list!.querySelectorAll('option')).map((o) => o.value); + expect(opts).toEqual(['personal', 'work', 'finance']); + const input = form.querySelector('#f-group') as HTMLInputElement; + expect(input.getAttribute('list')).toBe('groups-datalist'); + }); + + it('is a no-op if SW returns error', async () => { + const sendMessage = vi.fn().mockResolvedValue({ ok: false, error: 'vault_locked' }); + await wireGroupAutocomplete(form, { sendMessage }); + const input = form.querySelector('#f-group') as HTMLInputElement; + expect(input.getAttribute('list')).toBeNull(); + }); +}); diff --git a/extension/src/shared/form-affordances/group-autocomplete.ts b/extension/src/shared/form-affordances/group-autocomplete.ts new file mode 100644 index 0000000..60a4c89 --- /dev/null +++ b/extension/src/shared/form-affordances/group-autocomplete.ts @@ -0,0 +1,23 @@ +export interface GroupAutocompleteOpts { + sendMessage: (msg: { type: 'list_groups' }) => Promise<{ ok: boolean; data?: { groups: string[] }; error?: string }>; +} + +const DATALIST_ID = 'groups-datalist'; + +export async function wireGroupAutocomplete(form: HTMLElement, opts: GroupAutocompleteOpts): Promise { + const input = form.querySelector('#f-group'); + if (!input) return; + const resp = await opts.sendMessage({ type: 'list_groups' }); + if (!resp.ok || !resp.data) return; + + // Datalists must live in the document, not nested inside an input. Reuse if + // we've already mounted one this session. + let list = document.getElementById(DATALIST_ID) as HTMLDataListElement | null; + if (!list) { + list = document.createElement('datalist'); + list.id = DATALIST_ID; + document.body.appendChild(list); + } + list.innerHTML = resp.data.groups.map((g) => ``).join(''); + input.setAttribute('list', DATALIST_ID); +}