diff --git a/frontend/src/lib/tag/TagParentPicker.svelte.spec.ts b/frontend/src/lib/tag/TagParentPicker.svelte.spec.ts index ac78dac2..4db7d59c 100644 --- a/frontend/src/lib/tag/TagParentPicker.svelte.spec.ts +++ b/frontend/src/lib/tag/TagParentPicker.svelte.spec.ts @@ -199,4 +199,125 @@ describe('TagParentPicker – parent name subtitle', () => { // Only the tag name should appear (no subtitle) await expect.element(page.getByRole('option', { name: 'Haus' })).toBeInTheDocument(); }); + + it('shows the parentId as subtitle when allTags omits the parent', async () => { + mockFetchWithTags([{ id: 't2', name: 'Keller', parentId: 'unknown-parent-id' }]); + render(TagParentPicker, { name: 'parentId', allTags: [] }); + + const input = page.getByRole('combobox'); + await input.fill('K'); + await vi.advanceTimersByTimeAsync(300); + + // When parent not found in allTags, fallback shows the parentId itself + await expect.element(page.getByText('unknown-parent-id')).toBeInTheDocument(); + }); +}); + +describe('TagParentPicker – keyboard navigation', () => { + it('ArrowUp wraps around to last option', async () => { + mockFetchWithTags([ + { id: 't1', name: 'Haus' }, + { id: 't2', name: 'Garten' } + ]); + render(TagParentPicker, { name: 'parentId' }); + + const input = page.getByRole('combobox'); + await input.fill('a'); + await vi.advanceTimersByTimeAsync(300); + + const el = await input.element(); + // Without prior arrow-down, ArrowUp from -1 wraps via modular arithmetic + el.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true }) + ); + await vi.advanceTimersByTimeAsync(0); + + expect(el.getAttribute('aria-activedescendant')).toBeTruthy(); + }); + + it('Escape closes the dropdown', async () => { + mockFetchWithTags([{ id: 't1', name: 'Haus' }]); + render(TagParentPicker, { name: 'parentId' }); + + const input = page.getByRole('combobox'); + await input.fill('H'); + await vi.advanceTimersByTimeAsync(300); + + const el = await input.element(); + el.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }) + ); + await vi.advanceTimersByTimeAsync(0); + + // Listbox should be gone + expect(document.querySelector('[role="listbox"]')).toBeNull(); + }); + + it('Enter without active selection does nothing', async () => { + mockFetchWithTags([{ id: 't1', name: 'Haus' }]); + render(TagParentPicker, { name: 'parentId' }); + + const input = page.getByRole('combobox'); + await input.fill('H'); + await vi.advanceTimersByTimeAsync(300); + + const el = await input.element(); + el.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }) + ); + await vi.advanceTimersByTimeAsync(0); + + // Hidden input should still be empty + expect(hiddenInput('parentId')?.value).toBe(''); + }); + + it('keydown with no active dropdown is a no-op', async () => { + render(TagParentPicker, { name: 'parentId' }); + + const input = page.getByRole('combobox'); + const el = await input.element(); + expect(() => + el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + ).not.toThrow(); + }); + + it('Enter with active selection selects the highlighted tag', async () => { + mockFetchWithTags([ + { id: 't1', name: 'Haus' }, + { id: 't2', name: 'Garten' } + ]); + render(TagParentPicker, { name: 'parentId' }); + + const input = page.getByRole('combobox'); + await input.fill('a'); + await vi.advanceTimersByTimeAsync(300); + + const el = await input.element(); + el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + await vi.advanceTimersByTimeAsync(0); + el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + await vi.advanceTimersByTimeAsync(0); + + // Some tag selected + expect(hiddenInput('parentId')?.value).toBeTruthy(); + }); +}); + +describe('TagParentPicker – excludeIds filter', () => { + it('filters out tags whose id is in excludeIds', async () => { + mockFetchWithTags([ + { id: 't1', name: 'Haus' }, + { id: 't2', name: 'Keller' } + ]); + render(TagParentPicker, { name: 'parentId', excludeIds: ['t1'] }); + + const input = page.getByRole('combobox'); + await input.fill('a'); + await vi.advanceTimersByTimeAsync(300); + + // Only Keller should be visible + const options = document.querySelectorAll('[role="option"]'); + expect(options.length).toBe(1); + expect(options[0].textContent).toContain('Keller'); + }); });