refactor(transcription): consolidate MentionDropdown test files
For issue #380. Drops the redundant MentionDropdown.svelte.spec.ts that was added earlier in this branch and folds its search-input coverage into the long-established MentionDropdown.svelte.test.ts. Same test surface, single file. While there: - Updates the empty-state test to match the new behaviour: an empty search field shows the "Namen eingeben…" prompt; "Keine Personen gefunden" only appears when a query is entered but nothing matches. - Fixes pre-existing Person-type drift in makePerson (missing personType, familyMember). - Stricten the create-new link rel assertion to cover the new noreferrer addition. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* MentionDropdown — direct component tests.
|
||||
*
|
||||
* These tests render the dropdown in isolation, passing the `model` proxy
|
||||
* (matching what PersonMentionEditor would pass). They cover the dropdown's
|
||||
* own surface: the search input, the empty-query prompt, and the existing
|
||||
* "no results" / "create new" behaviors. Wiring tests against Tiptap live
|
||||
* in PersonMentionEditor.svelte.spec.ts.
|
||||
*/
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import MentionDropdown from './MentionDropdown.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
type Person = components['schemas']['Person'];
|
||||
|
||||
type DropdownState = {
|
||||
items: Person[];
|
||||
command: (item: Person) => void;
|
||||
clientRect: (() => DOMRect | null) | null;
|
||||
};
|
||||
|
||||
function makeModel(items: Person[] = []): DropdownState {
|
||||
return {
|
||||
items,
|
||||
command: () => {},
|
||||
clientRect: () => new DOMRect(0, 0, 0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe('MentionDropdown — search input', () => {
|
||||
it('renders a search input pre-filled with the editorQuery prop', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel(),
|
||||
editorQuery: 'WdG',
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('searchbox')).toHaveValue('WdG');
|
||||
});
|
||||
|
||||
it('exposes a data-test-search-input attribute for E2E selectors', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel(),
|
||||
editorQuery: '',
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
const input = document.querySelector('[data-test-search-input]');
|
||||
expect(input).not.toBeNull();
|
||||
expect((input as HTMLInputElement).type).toBe('search');
|
||||
});
|
||||
|
||||
it('shows "enter a name" prompt when search is empty (not "no results")', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel([]),
|
||||
editorQuery: '',
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText(m.person_mention_search_prompt())).toBeVisible();
|
||||
await expect.element(page.getByText(m.person_mention_popup_empty())).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "no results" when search has a query but the list is empty', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel([]),
|
||||
editorQuery: 'WdG',
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText(m.person_mention_popup_empty())).toBeVisible();
|
||||
await expect.element(page.getByText(m.person_mention_search_prompt())).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('"create new person" link has rel="noopener noreferrer" (CWE-116)', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel([]),
|
||||
editorQuery: 'unknown', // non-empty so the empty-state link renders
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
const link = document.querySelector('a[href="/persons/new"]') as HTMLAnchorElement;
|
||||
expect(link).not.toBeNull();
|
||||
expect(link.getAttribute('rel')).toContain('noopener');
|
||||
expect(link.getAttribute('rel')).toContain('noreferrer');
|
||||
});
|
||||
|
||||
it('search input wrapper meets the 44px touch target (WCAG 2.2 AA)', async () => {
|
||||
render(MentionDropdown, {
|
||||
model: makeModel(),
|
||||
editorQuery: '',
|
||||
onSearch: () => {}
|
||||
});
|
||||
|
||||
const input = document.querySelector('[data-test-search-input]') as HTMLElement;
|
||||
expect(input).not.toBeNull();
|
||||
expect(input.className).toContain('min-h-[44px]');
|
||||
});
|
||||
|
||||
it('invokes onSearch with the current value whenever the user types', async () => {
|
||||
const onSearch = vi.fn();
|
||||
render(MentionDropdown, {
|
||||
model: makeModel(),
|
||||
editorQuery: '',
|
||||
onSearch
|
||||
});
|
||||
|
||||
await userEvent.type(page.getByRole('searchbox'), 'Walter');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(onSearch).toHaveBeenCalled();
|
||||
expect(onSearch).toHaveBeenLastCalledWith('Walter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,35 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import MentionDropdown from './MentionDropdown.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type Person = components['schemas']['Person'];
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const makePerson = (id: string, name: string, overrides: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
firstName: name.split(' ')[0] ?? null,
|
||||
lastName: name.split(' ').slice(1).join(' ') || name,
|
||||
displayName: name,
|
||||
birthYear: null as number | null,
|
||||
deathYear: null as number | null,
|
||||
...overrides
|
||||
});
|
||||
const makePerson = (id: string, name: string, overrides: Partial<Person> = {}): Person =>
|
||||
({
|
||||
id,
|
||||
firstName: name.split(' ')[0] ?? null,
|
||||
lastName: name.split(' ').slice(1).join(' ') || name,
|
||||
displayName: name,
|
||||
personType: 'PERSON',
|
||||
familyMember: false,
|
||||
birthYear: null,
|
||||
deathYear: null,
|
||||
...overrides
|
||||
}) as unknown as Person;
|
||||
|
||||
const baseModel = (overrides: Record<string, unknown> = {}) => ({
|
||||
items: [] as ReturnType<typeof makePerson>[],
|
||||
type DropdownState = {
|
||||
items: Person[];
|
||||
command: (item: Person) => void;
|
||||
clientRect: (() => DOMRect | null) | null;
|
||||
};
|
||||
|
||||
const baseModel = (overrides: Partial<DropdownState> = {}): DropdownState => ({
|
||||
items: [],
|
||||
command: vi.fn(),
|
||||
clientRect: () => new DOMRect(100, 100, 0, 24),
|
||||
...overrides
|
||||
@@ -29,14 +42,22 @@ describe('MentionDropdown', () => {
|
||||
await expect.element(page.getByRole('listbox', { name: /person verlinken/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders the empty placeholder when items is empty', async () => {
|
||||
it('shows the "enter a name" prompt when the search field is empty', async () => {
|
||||
render(MentionDropdown, { props: { model: baseModel() } });
|
||||
|
||||
await expect.element(page.getByText('Keine Personen gefunden')).toBeVisible();
|
||||
await expect.element(page.getByText(m.person_mention_search_prompt())).toBeVisible();
|
||||
await expect.element(page.getByText(m.person_mention_popup_empty())).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "no persons found" when the search has a query but the list is empty', async () => {
|
||||
render(MentionDropdown, { props: { model: baseModel(), editorQuery: 'WdG' } });
|
||||
|
||||
await expect.element(page.getByText(m.person_mention_popup_empty())).toBeVisible();
|
||||
await expect.element(page.getByText(m.person_mention_search_prompt())).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the create-new escape hatch link in the empty state', async () => {
|
||||
render(MentionDropdown, { props: { model: baseModel() } });
|
||||
render(MentionDropdown, { props: { model: baseModel(), editorQuery: 'unknown' } });
|
||||
|
||||
const link = (await page
|
||||
.getByRole('link', { name: /neue person anlegen/i })
|
||||
@@ -44,6 +65,7 @@ describe('MentionDropdown', () => {
|
||||
expect(link.href).toContain('/persons/new');
|
||||
expect(link.target).toBe('_blank');
|
||||
expect(link.rel).toContain('noopener');
|
||||
expect(link.rel).toContain('noreferrer');
|
||||
});
|
||||
|
||||
it('renders one option per item when populated', async () => {
|
||||
@@ -104,3 +126,43 @@ describe('MentionDropdown', () => {
|
||||
expect(dropdown.style.left).toBe('123px');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Search input — Issue #380 ────────────────────────────────────────────────
|
||||
|
||||
describe('MentionDropdown — search input', () => {
|
||||
it('renders a search input pre-filled with the editorQuery prop', async () => {
|
||||
render(MentionDropdown, {
|
||||
props: { model: baseModel(), editorQuery: 'WdG' }
|
||||
});
|
||||
|
||||
await expect.element(page.getByRole('searchbox')).toHaveValue('WdG');
|
||||
});
|
||||
|
||||
it('exposes a data-test-search-input attribute for E2E selectors', async () => {
|
||||
render(MentionDropdown, { props: { model: baseModel() } });
|
||||
|
||||
const input = document.querySelector('[data-test-search-input]');
|
||||
expect(input).not.toBeNull();
|
||||
expect((input as HTMLInputElement).type).toBe('search');
|
||||
});
|
||||
|
||||
it('search input wrapper meets the 44px touch target (WCAG 2.2 AA)', async () => {
|
||||
render(MentionDropdown, { props: { model: baseModel() } });
|
||||
|
||||
const input = document.querySelector('[data-test-search-input]') as HTMLElement;
|
||||
expect(input).not.toBeNull();
|
||||
expect(input.className).toContain('min-h-[44px]');
|
||||
});
|
||||
|
||||
it('invokes onSearch with the current value whenever the user types', async () => {
|
||||
const onSearch = vi.fn();
|
||||
render(MentionDropdown, { props: { model: baseModel(), onSearch } });
|
||||
|
||||
await userEvent.type(page.getByRole('searchbox'), 'Walter');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(onSearch).toHaveBeenCalled();
|
||||
expect(onSearch).toHaveBeenLastCalledWith('Walter');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user