From 34ab8a0a2c59f4f349bd8dec41f9e4d4035baa8b Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 3 May 2026 08:41:11 +0200 Subject: [PATCH] test(geschichten): cover multi-person AND filter end-to-end Adds a Playwright flow that picks two persons through the typeahead, asserts both ?personId= params end up in the URL with two chips on screen, then removes the first chip and verifies only the second person id remains. Also extends .prettierignore so a stale root-owned test-results directory left over from running tests inside Docker doesn't break the pre-commit lint hook. --- frontend/.prettierignore | 1 + frontend/e2e/geschichten.spec.ts | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/frontend/.prettierignore b/frontend/.prettierignore index d6b62635..0902f412 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -23,5 +23,6 @@ bun.lockb # Test artifacts /test-results/ +/test-results.locked/ /e2e/.auth/ /coverage/ diff --git a/frontend/e2e/geschichten.spec.ts b/frontend/e2e/geschichten.spec.ts index 14aad908..1a05abb2 100644 --- a/frontend/e2e/geschichten.spec.ts +++ b/frontend/e2e/geschichten.spec.ts @@ -62,6 +62,51 @@ test.describe('Geschichten — writer + reader journey', () => { await expect(page.locator('article')).toBeVisible(); }); + test('multi-person filter: chips, URL params, and AND removal work end-to-end', async ({ + page + }) => { + await page.goto('/geschichten'); + await page.waitForSelector('[data-hydrated]'); + + // We need two persons to filter by. Open the picker and pick one whose name + // the dev seed reliably contains. Then open the picker again and pick a + // second one. Picking is via the typeahead — we type, wait for the listbox, + // click the first option. + async function pickPerson(query: string) { + await page.getByRole('button', { name: /Person wählen/ }).click(); + const input = page.getByRole('combobox', { name: /Person wählen/ }); + await input.fill(query); + // Wait for at least one option in the listbox, then click it + const firstOption = page.getByRole('option').first(); + await expect(firstOption).toBeVisible(); + await firstOption.click(); + } + + await pickPerson('a'); + await page.waitForURL(/personId=/); + const firstUrl = new URL(page.url()); + const firstIds = firstUrl.searchParams.getAll('personId'); + expect(firstIds).toHaveLength(1); + + await pickPerson('b'); + await page.waitForURL((url) => url.searchParams.getAll('personId').length === 2); + const secondUrl = new URL(page.url()); + const secondIds = secondUrl.searchParams.getAll('personId'); + expect(secondIds).toHaveLength(2); + expect(secondIds[0]).toBe(firstIds[0]); // first one persists + expect(secondIds[1]).not.toBe(firstIds[0]); // second is different + + // Two chips visible — find them by their remove-aria-label pattern + const chipButtons = page.getByRole('button', { name: /aus Filter entfernen/ }); + await expect(chipButtons).toHaveCount(2); + + // Remove the first chip — URL drops to one param + await chipButtons.first().click(); + await page.waitForURL((url) => url.searchParams.getAll('personId').length === 1); + const finalIds = new URL(page.url()).searchParams.getAll('personId'); + expect(finalIds).toEqual([secondIds[1]]); + }); + test('AxeBuilder finds no critical violations on the index', async ({ page }) => { await page.goto('/geschichten'); await page.waitForSelector('[data-hydrated]');