import { test, expect } from '@playwright/test'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * Document management E2E tests. * Assumes auth setup has run (storageState is applied by playwright.config.ts). * Assumes the backend has at least one document in the database. */ test.describe('Document list', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); // Wait for SvelteKit hydration to complete so onclick/oninput handlers are active. await page.waitForSelector('[data-hydrated]'); }); test('renders the search bar and document list', async ({ page }) => { await expect(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...')).toBeVisible(); await expect(page.getByRole('link', { name: /Neues Dokument/i })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/documents-home.png' }); }); test('navigation bar shows active state for Dokumente', async ({ page }) => { const navLink = page.getByRole('navigation').getByRole('link', { name: 'Dokumente' }); await expect(navLink).toHaveClass(/bg-nav-active/); }); test('text search filters the document list', async ({ page }) => { // Navigate directly with the query param — tests that search results are filtered // correctly without depending on the debounced oninput → goto chain in CI. await page.goto('/?q=zzz_unlikely_to_match_anything'); await expect(page.getByText('Keine Dokumente gefunden')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/documents-search-no-results.png' }); }); test('clearing the search returns all documents', async ({ page }) => { // Navigate with an active query first, then click the reset link. await page.goto('/?q=xyz_unlikely'); await page.getByTitle('Filter zurücksetzen').click(); await page.waitForURL('/'); await expect(page).toHaveURL('/'); await page.screenshot({ path: 'test-results/e2e/documents-reset-search.png' }); }); test('advanced filters panel opens and closes', async ({ page }) => { const btn = page.getByRole('button', { name: 'Filter', exact: true }); await btn.click(); await expect(page.getByLabel('Von')).toBeVisible(); await expect(page.getByLabel('Bis')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/documents-filters-open.png' }); await btn.click(); await expect(page.getByLabel('Von')).not.toBeVisible(); }); test('date range filter triggers a new search', async ({ page }) => { await page.getByRole('button', { name: 'Filter', exact: true }).click(); await page.getByLabel('Von').fill('2000-01-01'); await page.waitForURL(/from=2000-01-01/); await expect(page).toHaveURL(/from=2000-01-01/); await page.screenshot({ path: 'test-results/e2e/documents-date-filter.png' }); }); }); test.describe('Document detail', () => { test('clicking a document opens the detail page', async ({ page }) => { await page.goto('/'); // Click the first document link in the list const firstDoc = page.locator('ul li a').first(); const href = await firstDoc.getAttribute('href'); await firstDoc.click(); await expect(page).toHaveURL(href!); await page.screenshot({ path: 'test-results/e2e/document-detail.png' }); }); }); test.describe('New document', () => { test('renders the upload form with file input first', async ({ page }) => { await page.goto('/documents/new'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByRole('heading', { name: /Neues Dokument/i })).toBeVisible(); // File input comes before the title field in DOM order const fileInput = page.locator('input[type="file"]'); const titleInput = page.getByLabel('Titel'); await expect(fileInput).toBeVisible(); await expect(titleInput).toBeVisible(); const fileBox = await fileInput.boundingBox(); const titleBox = await titleInput.boundingBox(); expect(fileBox!.y).toBeLessThan(titleBox!.y); await page.screenshot({ path: 'test-results/e2e/document-new.png' }); }); test('title field is pre-filled from filename when a file is selected', async ({ page }) => { await page.goto('/documents/new'); await page.waitForSelector('[data-hydrated]'); const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles({ name: 'Brief_1965.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) }); await expect(page.getByLabel('Titel')).toHaveValue('Brief_1965'); await page.screenshot({ path: 'test-results/e2e/document-new-filename-prefill.png' }); }); test('typed title is not overwritten when a file is selected', async ({ page }) => { await page.goto('/documents/new'); await page.waitForSelector('[data-hydrated]'); await page.getByLabel('Titel').fill('Weihnachtsbrief 1965'); const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles({ name: 'Brief_1965.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) }); await expect(page.getByLabel('Titel')).toHaveValue('Weihnachtsbrief 1965'); await page.screenshot({ path: 'test-results/e2e/document-new-title-not-overwritten.png' }); }); }); test.describe('Document creation', () => { test('user fills in a title and lands on the new document detail page', async ({ page }) => { await page.goto('/documents/new'); await page.waitForSelector('[data-hydrated]'); await page.getByLabel('Titel').fill('E2E Testbrief'); await page.getByRole('button', { name: 'Speichern', exact: true }).click(); await expect(page).toHaveURL(/\/documents\/[^/]+$/); await expect(page.getByRole('heading', { name: 'E2E Testbrief' })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-create.png' }); }); test('user saves a document with only a file — title comes from filename', async ({ page }) => { await page.goto('/documents/new'); await page.waitForSelector('[data-hydrated]'); const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); await page.locator('input[type="file"]').setInputFiles({ name: 'Brief_1965.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) }); await page.getByRole('button', { name: 'Speichern', exact: true }).click(); await expect(page).toHaveURL(/\/documents\/[^/]+$/); await expect(page.getByRole('heading', { name: 'Brief_1965' })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-create-file-only.png' }); }); }); test.describe('Document editing', () => { test('user opens an existing document, changes the title, and sees the update', async ({ page }) => { // Find the document created in the previous describe await page.goto('/?q=E2E+Testbrief'); await page.waitForSelector('[data-hydrated]'); const docLink = page.getByRole('link', { name: 'E2E Testbrief' }).first(); const href = await docLink.getAttribute('href'); await page.goto(`${href}/edit`); await page.waitForSelector('[data-hydrated]'); await page.getByLabel('Titel').fill('E2E Testbrief (überarbeitet)'); await page.getByRole('button', { name: 'Speichern', exact: true }).click(); await expect(page).toHaveURL(/\/documents\/[^/]+$/); await expect(page.getByRole('heading', { name: 'E2E Testbrief (überarbeitet)' })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-edit-save.png' }); }); }); test.describe('Document edit', () => { test('renders the edit form with pre-filled data', async ({ page }) => { // Navigate to home, find first document, go to its edit page await page.goto('/'); const firstDocLink = page.locator('ul li a').first(); const href = await firstDocLink.getAttribute('href'); await page.goto(`${href}/edit`); await expect(page.getByRole('heading', { name: /Bearbeiten/i })).toBeVisible(); await expect(page.getByLabel('Titel')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-edit.png' }); }); test('shows a validation error for an invalid date format', async ({ page }) => { await page.goto('/'); const firstDocLink = page.locator('ul li a').first(); const href = await firstDocLink.getAttribute('href'); await page.goto(`${href}/edit`); // Wait for hydration so oninput={handleDateInput} is registered. await page.waitForSelector('[data-hydrated]'); const dateInput = page.getByLabel('Datum'); // Type partial digits: '99' → dateDisplay='99', dateIso='' → dateInvalid=true await dateInput.fill(''); await dateInput.pressSequentially('99'); await expect(page.getByText(/TT\.MM\.JJJJ/i)).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/document-edit-date-error.png' }); }); }); // ─── PDF Viewer ─────────────────────────────────────────────────────────────── const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); test.describe('PDF viewer', () => { let pdfDocHref: string; let noFileDocHref: string; test.beforeAll(async ({ request }) => { const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; // Create a document with a PDF file. const createRes = await request.post('/api/documents', { multipart: { title: 'E2E PDF Viewer Test' } }); if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); const doc = await createRes.json(); const uploadRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) } } }); if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); pdfDocHref = `${baseURL}/documents/${doc.id}`; // Create a document WITHOUT a file — used to verify no canvas is rendered. const noFileRes = await request.post('/api/documents', { multipart: { title: 'E2E No-File Test' } }); if (!noFileRes.ok()) throw new Error(`Create no-file document failed: ${noFileRes.status()}`); const noFileDoc = await noFileRes.json(); noFileDocHref = `${baseURL}/documents/${noFileDoc.id}`; }); test('PDF renders in the custom viewer — canvas is present instead of iframe', async ({ page }) => { await page.goto(pdfDocHref); await page.waitForSelector('[data-hydrated]'); // There must be NO iframe — we replaced it with PDF.js canvas rendering. await expect(page.locator('iframe')).not.toBeAttached(); // At least one canvas element must be visible (one per rendered page). await expect(page.locator('canvas').first()).toBeVisible({ timeout: 15000 }); await page.screenshot({ path: 'test-results/e2e/pdf-viewer-canvas.png' }); }); test('page navigation controls are visible', async ({ page }) => { await page.goto(pdfDocHref); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 15000 }); await expect(page.getByRole('button', { name: /prev|previous|zurück|vorige/i })).toBeVisible(); await expect(page.getByRole('button', { name: /next|weiter|nächste/i })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/pdf-viewer-nav.png' }); }); test('document without a file has no canvas', async ({ page }) => { // A document with no file attached must not render a PDF canvas. await page.goto(noFileDocHref); await page.waitForSelector('[data-hydrated]'); // No canvas — this document has no file await expect(page.locator('canvas')).not.toBeAttached(); await page.screenshot({ path: 'test-results/e2e/pdf-viewer-image-fallback.png' }); }); }); // ─── PDF Annotations (admin) ────────────────────────────────────────────────── // Shared with the read-only user describe block below let sharedAnnotationDocId: string; test.describe('PDF annotations — admin', () => { let annotationDocHref: string; test.beforeAll(async ({ request }) => { // Create a document with a PDF via API — much faster than UI automation. const createRes = await request.post('/api/documents', { multipart: { title: 'E2E Annotations Test' } }); if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); const doc = await createRes.json(); const uploadRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) } } }); if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; annotationDocHref = `${baseURL}/documents/${doc.id}`; sharedAnnotationDocId = doc.id; }); test('admin user sees an active Annotieren button on a PDF', async ({ page }) => { test.setTimeout(60_000); await page.goto(annotationDocHref); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); // Admin has ANNOTATE_ALL — button must be enabled const annotateBtn = page.getByRole('button', { name: /^annotieren$/i }); await expect(annotateBtn).toBeVisible(); await expect(annotateBtn).not.toBeDisabled(); await page.screenshot({ path: 'test-results/e2e/annotations-button-admin.png' }); }); test('admin can draw an annotation and it appears on the page', async ({ page }) => { test.setTimeout(60_000); await page.goto(annotationDocHref); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); // Enable annotate mode await page.getByRole('button', { name: /^annotieren$/i }).click(); // Color picker must appear await expect(page.getByLabel(/farbe/i)).toBeVisible(); // Draw on the annotation layer overlay const annotationLayer = page.locator('[role="presentation"]').last(); const box = await annotationLayer.boundingBox(); if (!box) throw new Error('Annotation layer not found'); const startX = box.x + box.width * 0.3; const startY = box.y + box.height * 0.3; const endX = box.x + box.width * 0.55; const endY = box.y + box.height * 0.55; await page.mouse.move(startX, startY); await page.mouse.down(); await page.mouse.move(endX, endY); await page.mouse.up(); await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({ timeout: 8000 }); await page.screenshot({ path: 'test-results/e2e/annotation-drawn.png' }); }); test('annotation persists after page reload', async ({ page }) => { test.setTimeout(60_000); await page.goto(annotationDocHref); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); // Annotation from the previous test must be loaded from the API await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({ timeout: 8000 }); await page.screenshot({ path: 'test-results/e2e/annotation-persisted.png' }); }); test('admin can delete an annotation', async ({ page }) => { test.setTimeout(60_000); await page.goto(annotationDocHref); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); // Ensure at least one annotation is visible before enabling annotate mode await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({ timeout: 8000 }); // Record count now — the draw test may have created more than one annotation const countBefore = await page.locator('[data-testid^="annotation-"]').count(); // Enable annotate mode to show delete buttons await page.getByRole('button', { name: /^annotieren$/i }).click(); const deleteBtn = page.getByRole('button', { name: /annotation löschen/i }).first(); await expect(deleteBtn).toBeVisible({ timeout: 8000 }); await deleteBtn.click(); await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(countBefore - 1, { timeout: 8000 }); await page.screenshot({ path: 'test-results/e2e/annotation-deleted.png' }); }); }); // ─── PDF Annotations — file hash (version awareness) ───────────────────────── test.describe('PDF annotations — file hash versioning', () => { const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; const PDF_FIXTURE2 = path.resolve(__dirname, 'fixtures/minimal2.pdf'); test('annotations are hidden after a different file is uploaded', async ({ page, request }) => { test.setTimeout(90_000); // 1. Create document and upload original PDF const createRes = await request.post('/api/documents', { multipart: { title: 'E2E Hash Test — version' } }); if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`); const doc = await createRes.json(); const uploadRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE) } } }); if (!uploadRes.ok()) throw new Error(`Upload failed: ${uploadRes.status()}`); // 2. Create an annotation via API const annotRes = await request.post(`/api/documents/${doc.id}/annotations`, { data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.2, height: 0.2, color: '#ff0000' } }); if (!annotRes.ok()) throw new Error(`Create annotation failed: ${annotRes.status()}`); // 3. Verify annotation appears before re-upload await page.goto(`${baseURL}/documents/${doc.id}`); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({ timeout: 8000 }); // 4. Upload a different file (different hash) const reuploadRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal2.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE2) } } }); if (!reuploadRes.ok()) throw new Error(`Re-upload failed: ${reuploadRes.status()}`); // 5. Reload — annotation must be hidden and notice shown await page.reload(); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); // Use :not() to exclude the outdated-notice element whose testid also starts with "annotation-" await expect( page.locator('[data-testid^="annotation-"]:not([data-testid="annotation-outdated-notice"])') ).toHaveCount(0, { timeout: 8000 }); await expect(page.locator('[data-testid="annotation-outdated-notice"]')).toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'test-results/e2e/annotation-hidden-after-reupload.png' }); }); test('annotations reappear after re-uploading the original file', async ({ page, request }) => { test.setTimeout(90_000); // 1. Create document and upload original PDF const createRes = await request.post('/api/documents', { multipart: { title: 'E2E Hash Test — restore' } }); if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`); const doc = await createRes.json(); const originalBytes = fs.readFileSync(PDF_FIXTURE); const uploadRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: originalBytes } } }); if (!uploadRes.ok()) throw new Error(`Upload failed: ${uploadRes.status()}`); // 2. Create annotation const annotRes = await request.post(`/api/documents/${doc.id}/annotations`, { data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.2, height: 0.2, color: '#0000ff' } }); if (!annotRes.ok()) throw new Error(`Create annotation failed: ${annotRes.status()}`); // 3. Replace with different file const replaceRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal2.pdf', mimeType: 'application/pdf', buffer: fs.readFileSync(PDF_FIXTURE2) } } }); if (!replaceRes.ok()) throw new Error(`Replace failed: ${replaceRes.status()}`); // 4. Re-upload original file (restoring the hash) const restoreRes = await request.put(`/api/documents/${doc.id}`, { multipart: { title: doc.title, file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: originalBytes } } }); if (!restoreRes.ok()) throw new Error(`Restore failed: ${restoreRes.status()}`); // 5. Verify annotation reappears and notice is gone await page.goto(`${baseURL}/documents/${doc.id}`); await page.waitForSelector('[data-hydrated]'); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({ timeout: 8000 }); await expect(page.locator('[data-testid="annotation-outdated-notice"]')).not.toBeVisible(); await page.screenshot({ path: 'test-results/e2e/annotation-restored.png' }); }); }); // ─── PDF Annotations (read-only user) ───────────────────────────────────────── test.describe('PDF annotations — read-only user', () => { // Isolated session — does not share the admin storage state test.use({ storageState: { cookies: [], origins: [] } }); test('read-only user does not see the Annotieren button', async ({ page }) => { test.setTimeout(60_000); await page.goto('/login'); await page.getByLabel('Benutzername').fill('reader'); await page.getByLabel('Passwort').fill('reader123'); await page.getByRole('button', { name: 'Anmelden' }).click(); await page.waitForURL('/'); // Navigate directly to the PDF document created by the admin beforeAll. const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; await page.goto(`${baseURL}/documents/${sharedAnnotationDocId}`); await page.waitForSelector('[data-hydrated]'); // Reader users do not have ANNOTATE_ALL permission — the button must not be shown at all. const annotateBtn = page.getByRole('button', { name: /annotieren/i }); await expect(annotateBtn).not.toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'test-results/e2e/annotations-button-reader.png' }); }); }); // ── J3: Edit document — add an existing tag ──────────────────────────────── // // Verifies that a user can open a document's edit page and assign a tag using // the TagInput component, then save and see the tag link on the detail page. // Seeds a unique tag via a throwaway document so the test never depends on the // seeded "Familie" tag (which admin tests rename during their lifecycle). test.describe('Document editing — tags (J3)', () => { let tagDocId: string; let seedDocId: string; let seededTagName: string; test.beforeAll(async ({ request }) => { const stamp = Date.now().toString(36); seededTagName = `E2E-J3-Tag-${stamp}`; // Create a throwaway document and associate the unique tag with it so it // exists in the system for the TagInput suggestion list. const seederRes = await request.post('/api/documents', { multipart: { title: `E2E J3 Tag Seeder ${stamp}` } }); if (!seederRes.ok()) throw new Error(`Create seeder failed: ${seederRes.status()}`); const seeder = await seederRes.json(); seedDocId = seeder.id; const seedTagRes = await request.put(`/api/documents/${seedDocId}`, { multipart: { title: seeder.title, tags: seededTagName } }); if (!seedTagRes.ok()) throw new Error(`Seed tag failed: ${seedTagRes.status()}`); // Create the test document without the tag — the test will add it. const createRes = await request.post('/api/documents', { multipart: { title: `E2E Tag Edit Test ${stamp}` } }); if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); const doc = await createRes.json(); tagDocId = doc.id; }); test.afterAll(async ({ request }) => { if (tagDocId) await request.delete(`/api/documents/${tagDocId}`); if (seedDocId) await request.delete(`/api/documents/${seedDocId}`); }); test('user adds an existing tag and sees it on the detail page', async ({ page }) => { await page.goto(`/documents/${tagDocId}/edit`); await page.waitForSelector('[data-hydrated]'); // TagInput has placeholder "Schlagworte hinzufügen..." when empty. const tagInput = page.getByPlaceholder('Schlagworte hinzufügen...'); await expect(tagInput).toBeVisible(); // Type the seeded tag name and wait for the suggestion. await tagInput.fill(seededTagName); const suggestion = page.getByRole('option', { name: seededTagName }).first(); await expect(suggestion).toBeVisible({ timeout: 5_000 }); await suggestion.click(); // Save the document. await page.getByRole('button', { name: 'Speichern', exact: true }).click(); // Redirected to detail page — the tag link must be visible in the metadata section. await expect(page).toHaveURL(/\/documents\/[^/]+$/); await expect(page.locator('a[href*="?tag="]', { hasText: seededTagName })).toBeVisible({ timeout: 5_000 }); await page.screenshot({ path: 'test-results/e2e/document-edit-tag.png' }); }); }); // ── J4: Create a brand-new tag via TagInput ──────────────────────────────── // // Types a tag name that does not exist yet, confirms creation with Enter, and // verifies the tag chip persists after save. test.describe('Document editing — new tag creation (J4)', () => { const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; let newTagDocHref: string; const newTagName = `E2E-Tag-${Date.now().toString(36)}`; test.beforeAll(async ({ request }) => { const createRes = await request.post('/api/documents', { multipart: { title: 'E2E New Tag Test' } }); if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); const doc = await createRes.json(); newTagDocHref = `${baseURL}/documents/${doc.id}`; }); test('user types a new tag name, presses Enter, saves, and sees the chip', async ({ page }) => { await page.goto(`${newTagDocHref}/edit`); await page.waitForSelector('[data-hydrated]'); const tagInput = page.getByPlaceholder('Schlagworte hinzufügen...'); await expect(tagInput).toBeVisible(); await tagInput.fill(newTagName); // Press Enter to confirm tag creation (TagInput creates on Enter when no option selected). await tagInput.press('Enter'); // The chip for the new tag should appear inside the TagInput immediately. await expect(page.getByText(newTagName)).toBeVisible({ timeout: 5_000 }); await page.getByRole('button', { name: 'Speichern', exact: true }).click(); await expect(page).toHaveURL(/\/documents\/[^/]+$/); await expect(page.getByText(newTagName)).toBeVisible({ timeout: 5_000 }); await page.screenshot({ path: 'test-results/e2e/document-new-tag-created.png' }); }); }); // ── J6: Multi-filter search (text + tag) ────────────────────────────────── // // Verifies that combining a text query with a tag filter narrows results // correctly on the document search page. test.describe('Document search — multi-filter (J6)', () => { test('combining text search and tag filter shows only matching documents', async ({ page }) => { // Navigate with a text query + a tag filter param. // We use the seeded "Familie" tag (slug "familie") and a text that is unlikely // to match anything — confirming that the AND combination works. await page.goto('/?q=zzz_unlikely&tagId=nonexistent-tag-id'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByText('Keine Dokumente gefunden')).toBeVisible({ timeout: 5_000 }); // Now navigate with just the text query — should also have no results for the noise string. await page.goto('/?q=zzz_unlikely'); await expect(page.getByText('Keine Dokumente gefunden')).toBeVisible({ timeout: 5_000 }); await page.screenshot({ path: 'test-results/e2e/document-multi-filter.png' }); }); test('date range + text query combination triggers a filtered search', async ({ page }) => { // Use two filter params together from the URL — both must appear in the URL // and the search must run without errors. await page.goto('/?q=E2E&from=2000-01-01'); await page.waitForSelector('[data-hydrated]'); // The URL must contain both params (confirming SvelteKit preserves them). await expect(page).toHaveURL(/q=E2E/); await expect(page).toHaveURL(/from=2000-01-01/); await page.screenshot({ path: 'test-results/e2e/document-multi-filter-date-text.png' }); }); });