test(shopping): E2E-Spec + clearShoppingCart-Fixture
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 33s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 23:50:05 +02:00
parent 0346a699b9
commit 4e902b1d98
2 changed files with 116 additions and 0 deletions

View File

@@ -65,3 +65,10 @@ export async function cleanupE2EComments(
}
}
}
/**
* Leert den haushaltsweiten Einkaufswagen. Idempotent.
*/
export async function clearShoppingCart(api: APIRequestContext): Promise<void> {
await api.delete('/api/shopping-list');
}

View File

@@ -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);
});
});