test(e2e): Playwright Smoketests gegen kochwas-dev (remote)
Automatisierte End-to-End-Tests gegen ein deployed Environment. Loest
die manuellen MCP-Playwright-Runs ab. 42 Tests in 9 Files:
- homepage: H1, Sektionen, Sort-Tabs, Console-Errors
- search: lokaler Treffer, Web-Fallback, Empty-State, Deep-Link
- profile: Switcher, Auswahl-Persistenz, Favoriten-Section, Guard-Dialog
- recipe-detail: Header, Portionen-Scaling (4->6), Favorit-Toggle,
Rating-Persistenz ueber Reload, Gekocht-Counter, Wunschliste-Toggle
- comments: eigenen erstellen+loeschen via UI, fremder hat kein Delete
- wishlist: Seite, Sort-Tabs, Badge-Sync, requireProfile-Custom-Message
- preview: Guard ohne ?url=, echte URL parst, unparsbare zeigt error-box
- admin: alle 4 Subrouten + /admin redirect
- api-errors: parsePositiveIntParam (4x Invalid id), validateBody (4x
Invalid body + issues), 404, Sanity /health /profiles /domains
Architektur:
- Separate playwright.remote.config.ts (getrennt von local preview)
- workers: 1 + afterEach API-Cleanup (rating, favorite, wishlist, comments)
- Hardcoded Recipe-ID 66 + Profile 1/2/3 — stabile Dev-DB-Seeds
- E2E_REMOTE_URL ueberschreibt die Ziel-URL
Ausfuehrung: npm run test:e2e:remote
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:14:04 +02:00
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
|
import { setActiveProfile, HENDRIK_ID } from './fixtures/profile';
|
|
|
|
|
import {
|
|
|
|
|
clearFavorite,
|
|
|
|
|
clearRating,
|
|
|
|
|
removeFromWishlist
|
|
|
|
|
} from './fixtures/api-cleanup';
|
|
|
|
|
|
|
|
|
|
// Chicken Teriyaki auf kochwas-dev: 4 Portionen, 500 g Haehnchen, 100 ml Soja.
|
|
|
|
|
const RECIPE_ID = 66;
|
|
|
|
|
|
|
|
|
|
test.describe('Rezept-Detail', () => {
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
await setActiveProfile(page, HENDRIK_ID);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
|
|
|
await clearRating(request, RECIPE_ID, HENDRIK_ID);
|
|
|
|
|
await clearFavorite(request, RECIPE_ID, HENDRIK_ID);
|
|
|
|
|
await removeFromWishlist(request, RECIPE_ID, HENDRIK_ID);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Header + Zutaten sichtbar', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByRole('heading', { level: 1, name: /Chicken Teriyaki/i })
|
|
|
|
|
).toBeVisible();
|
|
|
|
|
await expect(page.getByText('Hähnchenbrustfilet').first()).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Portionen-Scaler: 4 -> 6 skaliert Mengen proportional', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
// Start: 4 Portionen, 500 g Haehnchen, 100 ml Soja.
|
|
|
|
|
await expect(page.locator('.srv-value strong').first()).toHaveText('4');
|
|
|
|
|
await page.getByRole('button', { name: 'Mehr' }).first().click();
|
|
|
|
|
await page.getByRole('button', { name: 'Mehr' }).first().click();
|
|
|
|
|
await expect(page.locator('.srv-value strong').first()).toHaveText('6');
|
2026-04-19 12:21:36 +02:00
|
|
|
// Skalierte Mengen 1.5x — ueber das Item-Name-Filter, robuster
|
|
|
|
|
// gegenueber Whitespace-Quirks zwischen <span class="qty">-Teilen.
|
|
|
|
|
await expect(
|
|
|
|
|
page.locator('.ing-list li', { hasText: 'Hähnchenbrustfilet' })
|
|
|
|
|
).toContainText('750 g');
|
|
|
|
|
await expect(
|
|
|
|
|
page.locator('.ing-list li', { hasText: 'Sojasauce' })
|
|
|
|
|
).toContainText('150 ml');
|
test(e2e): Playwright Smoketests gegen kochwas-dev (remote)
Automatisierte End-to-End-Tests gegen ein deployed Environment. Loest
die manuellen MCP-Playwright-Runs ab. 42 Tests in 9 Files:
- homepage: H1, Sektionen, Sort-Tabs, Console-Errors
- search: lokaler Treffer, Web-Fallback, Empty-State, Deep-Link
- profile: Switcher, Auswahl-Persistenz, Favoriten-Section, Guard-Dialog
- recipe-detail: Header, Portionen-Scaling (4->6), Favorit-Toggle,
Rating-Persistenz ueber Reload, Gekocht-Counter, Wunschliste-Toggle
- comments: eigenen erstellen+loeschen via UI, fremder hat kein Delete
- wishlist: Seite, Sort-Tabs, Badge-Sync, requireProfile-Custom-Message
- preview: Guard ohne ?url=, echte URL parst, unparsbare zeigt error-box
- admin: alle 4 Subrouten + /admin redirect
- api-errors: parsePositiveIntParam (4x Invalid id), validateBody (4x
Invalid body + issues), 404, Sanity /health /profiles /domains
Architektur:
- Separate playwright.remote.config.ts (getrennt von local preview)
- workers: 1 + afterEach API-Cleanup (rating, favorite, wishlist, comments)
- Hardcoded Recipe-ID 66 + Profile 1/2/3 — stabile Dev-DB-Seeds
- E2E_REMOTE_URL ueberschreibt die Ziel-URL
Ausfuehrung: npm run test:e2e:remote
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:14:04 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Favorit toggelt heart-Klasse sauber', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
const favBtn = page.getByRole('button', { name: 'Favorit' });
|
|
|
|
|
await expect(favBtn).not.toHaveClass(/heart/);
|
|
|
|
|
await favBtn.click();
|
|
|
|
|
await expect(favBtn).toHaveClass(/heart/);
|
|
|
|
|
await favBtn.click();
|
|
|
|
|
await expect(favBtn).not.toHaveClass(/heart/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Rating persistiert ueber Reload', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
await page.getByRole('button', { name: '4 Sterne' }).click();
|
|
|
|
|
await expect(page.getByRole('button', { name: '4 Sterne' })).toHaveClass(/filled/);
|
|
|
|
|
await page.reload();
|
|
|
|
|
await expect(page.getByRole('button', { name: '4 Sterne' })).toHaveClass(/filled/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Heute gekocht inkrementiert Counter', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
const cookedBtn = page.getByRole('button', { name: /Heute gekocht/i });
|
|
|
|
|
const before = (await cookedBtn.innerText()).trim();
|
|
|
|
|
await cookedBtn.click();
|
|
|
|
|
// Der Button bekommt einen "(N)"-Suffix bzw. der existierende zaehler
|
|
|
|
|
// steigt. Wir pruefen nur, dass sich der Text aendert.
|
|
|
|
|
await expect(cookedBtn).not.toHaveText(before);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Auf Wunschliste-Toggle funktioniert', async ({ page }) => {
|
|
|
|
|
await page.goto(`/recipes/${RECIPE_ID}`);
|
|
|
|
|
const wishBtn = page.getByRole('button', { name: /Auf Wunschliste/i });
|
|
|
|
|
const initialLabel = (await wishBtn.getAttribute('aria-label')) ?? '';
|
|
|
|
|
await wishBtn.click();
|
|
|
|
|
// aria-label wechselt zwischen "setzen" und "Von der Wunschliste entfernen"
|
|
|
|
|
await expect(wishBtn).not.toHaveAttribute('aria-label', initialLabel);
|
|
|
|
|
});
|
|
|
|
|
});
|