refactor(transcription): consolidate MentionDropdown test files
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m27s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 59s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m27s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 59s
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 { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page } from 'vitest/browser';
|
import { page, userEvent } from 'vitest/browser';
|
||||||
import MentionDropdown from './MentionDropdown.svelte';
|
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);
|
afterEach(cleanup);
|
||||||
|
|
||||||
const makePerson = (id: string, name: string, overrides: Record<string, unknown> = {}) => ({
|
const makePerson = (id: string, name: string, overrides: Partial<Person> = {}): Person =>
|
||||||
|
({
|
||||||
id,
|
id,
|
||||||
firstName: name.split(' ')[0] ?? null,
|
firstName: name.split(' ')[0] ?? null,
|
||||||
lastName: name.split(' ').slice(1).join(' ') || name,
|
lastName: name.split(' ').slice(1).join(' ') || name,
|
||||||
displayName: name,
|
displayName: name,
|
||||||
birthYear: null as number | null,
|
personType: 'PERSON',
|
||||||
deathYear: null as number | null,
|
familyMember: false,
|
||||||
|
birthYear: null,
|
||||||
|
deathYear: null,
|
||||||
...overrides
|
...overrides
|
||||||
});
|
}) as unknown as Person;
|
||||||
|
|
||||||
const baseModel = (overrides: Record<string, unknown> = {}) => ({
|
type DropdownState = {
|
||||||
items: [] as ReturnType<typeof makePerson>[],
|
items: Person[];
|
||||||
|
command: (item: Person) => void;
|
||||||
|
clientRect: (() => DOMRect | null) | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseModel = (overrides: Partial<DropdownState> = {}): DropdownState => ({
|
||||||
|
items: [],
|
||||||
command: vi.fn(),
|
command: vi.fn(),
|
||||||
clientRect: () => new DOMRect(100, 100, 0, 24),
|
clientRect: () => new DOMRect(100, 100, 0, 24),
|
||||||
...overrides
|
...overrides
|
||||||
@@ -29,14 +42,22 @@ describe('MentionDropdown', () => {
|
|||||||
await expect.element(page.getByRole('listbox', { name: /person verlinken/i })).toBeVisible();
|
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() } });
|
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 () => {
|
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
|
const link = (await page
|
||||||
.getByRole('link', { name: /neue person anlegen/i })
|
.getByRole('link', { name: /neue person anlegen/i })
|
||||||
@@ -44,6 +65,7 @@ describe('MentionDropdown', () => {
|
|||||||
expect(link.href).toContain('/persons/new');
|
expect(link.href).toContain('/persons/new');
|
||||||
expect(link.target).toBe('_blank');
|
expect(link.target).toBe('_blank');
|
||||||
expect(link.rel).toContain('noopener');
|
expect(link.rel).toContain('noopener');
|
||||||
|
expect(link.rel).toContain('noreferrer');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders one option per item when populated', async () => {
|
it('renders one option per item when populated', async () => {
|
||||||
@@ -104,3 +126,43 @@ describe('MentionDropdown', () => {
|
|||||||
expect(dropdown.style.left).toBe('123px');
|
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