test(tag): expand TagParentPicker keyboard + excludeIds coverage
ArrowUp wrap-around, Escape close, Enter without selection no-op, keydown without dropdown no-throw, Enter with active selection selects, excludeIds filter works, parentId fallback as subtitle. 7 new tests covering ~12 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -199,4 +199,125 @@ describe('TagParentPicker – parent name subtitle', () => {
|
|||||||
// Only the tag name should appear (no subtitle)
|
// Only the tag name should appear (no subtitle)
|
||||||
await expect.element(page.getByRole('option', { name: 'Haus' })).toBeInTheDocument();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user