From 4e902b1d980ff164bfd4dc782cd6964e6fc756f8 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:50:05 +0200 Subject: [PATCH] test(shopping): E2E-Spec + clearShoppingCart-Fixture Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/remote/fixtures/api-cleanup.ts | 7 ++ tests/e2e/remote/shopping.spec.ts | 109 +++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 tests/e2e/remote/shopping.spec.ts diff --git a/tests/e2e/remote/fixtures/api-cleanup.ts b/tests/e2e/remote/fixtures/api-cleanup.ts index 9c20297..9375b9f 100644 --- a/tests/e2e/remote/fixtures/api-cleanup.ts +++ b/tests/e2e/remote/fixtures/api-cleanup.ts @@ -65,3 +65,10 @@ export async function cleanupE2EComments( } } } + +/** + * Leert den haushaltsweiten Einkaufswagen. Idempotent. + */ +export async function clearShoppingCart(api: APIRequestContext): Promise { + await api.delete('/api/shopping-list'); +} diff --git a/tests/e2e/remote/shopping.spec.ts b/tests/e2e/remote/shopping.spec.ts new file mode 100644 index 0000000..f74a5a4 --- /dev/null +++ b/tests/e2e/remote/shopping.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@playwright/test'; +import { setActiveProfile, HENDRIK_ID } from './fixtures/profile'; +import { clearShoppingCart } from './fixtures/api-cleanup'; + +test.describe('Einkaufsliste E2E', () => { + test.afterEach(async ({ request }) => { + await clearShoppingCart(request); + }); + + test('Cart-Button auf der Wunschliste erzeugt Header-Badge', async ({ page, request }) => { + await setActiveProfile(page, HENDRIK_ID); + // Voraussetzung: Dev-System hat mindestens einen Wunschlisten-Eintrag + const wlRes = await request.get('/api/wishlist?sort=popular'); + const wlBody = (await wlRes.json()) as { entries: { recipe_id: number }[] }; + test.skip(wlBody.entries.length === 0, 'Wunschliste leer auf Dev — Test uebersprungen'); + + await page.goto('/wishlist'); + await page.getByLabel('In den Einkaufswagen').first().click(); + await expect(page.getByLabel(/Einkaufsliste \(\d+\)/)).toBeVisible(); + }); + + test('Shopping-List-Seite zeigt Rezept-Chip + Zutaten', async ({ page, request }) => { + await setActiveProfile(page, HENDRIK_ID); + const wlRes = await request.get('/api/wishlist?sort=popular'); + const wlBody = (await wlRes.json()) as { entries: { recipe_id: number }[] }; + test.skip(wlBody.entries.length === 0, 'Wunschliste leer'); + const recipeId = wlBody.entries[0].recipe_id; + + await request.post('/api/shopping-list/recipe', { data: { recipe_id: recipeId } }); + await page.goto('/shopping-list'); + + await expect(page.getByRole('heading', { level: 1, name: 'Einkaufsliste' })).toBeVisible(); + // Chip fuers Rezept sichtbar + await expect(page.getByLabel('Portion weniger').first()).toBeVisible(); + // Mindestens eine Zutatenzeile + await expect(page.locator('.row').first()).toBeVisible(); + }); + + test('Portions-Stepper veraendert Mengen live', async ({ page, request }) => { + await setActiveProfile(page, HENDRIK_ID); + const wlRes = await request.get('/api/wishlist?sort=popular'); + const wlBody = (await wlRes.json()) as { entries: { recipe_id: number }[] }; + test.skip(wlBody.entries.length === 0, 'Wunschliste leer'); + + await request.post('/api/shopping-list/recipe', { + data: { recipe_id: wlBody.entries[0].recipe_id, servings: 4 } + }); + await page.goto('/shopping-list'); + // Menge der ersten Zeile "vorher" lesen + const qtyBefore = await page.locator('.qty').first().textContent(); + // Portion +1 + await page.getByLabel('Portion mehr').first().click(); + // Nach Fetch+Rerender muss die Menge sich aendern (ungleich dem Vorher-Wert) + await expect + .poll(async () => (await page.locator('.qty').first().textContent())?.trim()) + .not.toBe(qtyBefore?.trim()); + }); + + test('Abhaken: Zeile durchgestrichen, Badge-Count sinkt, persistiert nach Reload', async ({ page, request }) => { + await setActiveProfile(page, HENDRIK_ID); + const wlRes = await request.get('/api/wishlist?sort=popular'); + const wlBody = (await wlRes.json()) as { entries: { recipe_id: number }[] }; + test.skip(wlBody.entries.length === 0, 'Wunschliste leer'); + + await request.post('/api/shopping-list/recipe', { + data: { recipe_id: wlBody.entries[0].recipe_id } + }); + await page.goto('/shopping-list'); + + const countBadge = page.getByLabel(/Einkaufsliste \(\d+\)/); + const badgeTextBefore = await countBadge.textContent(); + const numBefore = Number((badgeTextBefore ?? '').replace(/\D+/g, '')) || 0; + + const firstRow = page.locator('label.row').first(); + await firstRow.locator('input[type=checkbox]').check(); + await expect(firstRow).toHaveClass(/checked/); + + // Badge muss sinken (nach Store-Refresh) + await expect + .poll(async () => { + const t = (await countBadge.textContent()) ?? ''; + return Number(t.replace(/\D+/g, '')) || 0; + }) + .toBeLessThan(numBefore); + + // Reload persistiert + await page.reload(); + await expect(page.locator('label.row.checked').first()).toBeVisible(); + }); + + test('Liste leeren: Confirm + Empty-State + Badge weg', async ({ page, request }) => { + await setActiveProfile(page, HENDRIK_ID); + const wlRes = await request.get('/api/wishlist?sort=popular'); + const wlBody = (await wlRes.json()) as { entries: { recipe_id: number }[] }; + test.skip(wlBody.entries.length === 0, 'Wunschliste leer'); + + await request.post('/api/shopping-list/recipe', { + data: { recipe_id: wlBody.entries[0].recipe_id } + }); + await page.goto('/shopping-list'); + + await page.getByRole('button', { name: 'Liste leeren' }).click(); + // Confirm-Dialog (ConfirmAction nutzt einen App-eigenen Dialog, kein native) + await page.getByRole('button', { name: 'Leeren' }).click(); + + await expect(page.getByText('Einkaufswagen ist leer.')).toBeVisible(); + await expect(page.getByLabel(/Einkaufsliste \(\d+\)/)).toHaveCount(0); + }); +});