import AxeBuilder from '@axe-core/playwright'; import { test, expect } from '@playwright/test'; // NL search is mocked at the network boundary — Ollama is not required in CI. // CSRF enforcement is bypassed by page.route (the real request is never sent), // so it is only verified in manual full-stack runs (see issue #739 DevOps notes). const interpretation = { resolvedPersons: [ { id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter Raddatz' }, { id: '22222222-2222-2222-2222-222222222222', displayName: 'Emma Raddatz' } ], ambiguousPersons: [], dateFrom: '1914-01-01', dateTo: '1918-12-31', keywords: ['krieg'], resolvedTags: [{ id: '33333333-3333-3333-3333-333333333333', name: 'Weltkrieg', color: 'sage' }], rawQuery: 'Was hat Walter an Emma im Krieg geschrieben?', keywordsApplied: true, tagsApplied: true }; const nlResponse = { result: { items: [], totalElements: 0, pageNumber: 0, pageSize: 20, totalPages: 0, undatedCount: 0 }, interpretation }; test.describe('NL (smart) search — happy path', () => { test('toggle → loading → chips → remove chip re-runs keyword search; axe clean light + dark', async ({ page }) => { // Deliberate delay so the loading state is assertable before the response arrives. await page.route('**/api/search/nl', async (route) => { await new Promise((resolve) => setTimeout(resolve, 150)); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(nlResponse) }); }); await page.goto('/documents'); await page.waitForSelector('[data-hydrated]'); // Switch to smart mode via the toggle pill (keyword label = "Text"). await page.getByRole('button', { name: /Text/ }).click(); const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…'); await input.fill('Was hat Walter an Emma im Krieg geschrieben?'); await input.press('Enter'); // Loading panel announced to screen readers. await expect(page.getByText(/Archiv wird befragt/)).toBeVisible(); // Directional chip (Walter → Emma) + keyword chip render once the fixture resolves. await expect(page.getByText('→')).toBeVisible(); await expect(page.getByText('Stichwort: krieg')).toBeVisible(); // Accessibility — light mode. const lightScan = await new AxeBuilder({ page }) .include('[data-testid="smart-search-results"]') .analyze(); expect(lightScan.violations).toEqual([]); // Accessibility — dark mode. await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); const darkScan = await new AxeBuilder({ page }) .include('[data-testid="smart-search-results"]') .analyze(); expect(darkScan.violations).toEqual([]); await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light')); // Removing the keyword chip re-runs a keyword GET with the remaining resolved // params (sender + receiver from the directional pair). await page.getByRole('button', { name: 'Filter entfernen: Stichwort: krieg' }).click(); await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/); await expect(page).toHaveURL(/receiverId=22222222-2222-2222-2222-222222222222/); }); });