Files
YesChef/test-e2e.mjs
T
Josh Rogers 48d30df07b Initial commit: YesChef family shopping list and recipe app
Backend (.NET 10 minimal API):
- Vertical slice architecture with feature folders
- Postgres via EF Core with initial migration
- JWT auth with family invite code registration
- REST endpoints for stores, shopping lists, items, recipes
- SignalR hub for real-time list collaboration (per-list groups
  and lists-overview group for live list creation/archival/progress)
- Multi-stage Dockerfile

Frontend (SvelteKit + Svelte 5 runes, Tailwind v4):
- Mobile-first PWA with web manifest and service worker
- Bottom-nav layout, login/register, lists overview, list detail,
  stores management, recipes (list/create/detail with add-to-list)
- SignalR client with reference-counted connection
- Real-time updates on both lists overview and list detail pages

Infrastructure:
- docker-compose.yml with postgres, backend, frontend services
  and Traefik labels for path-based routing (/api, /hubs to backend)
- .env.example with required config

End-to-end tests (Playwright):
- test-e2e.mjs: single-user flow (auth, stores, lists, items, recipes)
- test-e2e-multiuser.mjs: two-user real-time sync coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 19:32:39 -05:00

148 lines
5.1 KiB
JavaScript

import { chromium } from 'playwright';
const browser = await chromium.launch({ channel: 'chrome', headless: false, slowMo: 300 });
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
const page = await context.newPage();
const BASE = 'http://localhost:5173';
let failed = false;
async function step(name, fn) {
process.stdout.write(`${name}... `);
try {
await fn();
console.log('✅');
} catch (e) {
console.log(`${e.message}`);
failed = true;
throw e;
}
}
try {
await step('Navigate to app → should redirect to /login', async () => {
await page.goto(BASE);
await page.waitForURL('**/login', { timeout: 5000 });
});
await step('Register a new account', async () => {
await page.click('text=Create account');
await page.fill('input[placeholder="Name"]', 'TestChef');
await page.fill('input[placeholder="Password"]', 'password123');
await page.fill('input[placeholder="Family code"]', 'dev-family-code');
await page.click('button[type="submit"]');
await page.waitForURL('**/lists', { timeout: 5000 });
});
await step('Create store "Kroger"', async () => {
await page.click('a:has-text("Stores")');
await page.waitForURL('**/stores');
await page.fill('input[placeholder="New store name"]', 'Kroger');
await page.click('button:has-text("Add")');
await page.waitForSelector('text=Kroger');
});
await step('Create store "Costco"', async () => {
await page.fill('input[placeholder="New store name"]', 'Costco');
await page.click('button:has-text("Add")');
await page.waitForSelector('text=Costco');
});
await step('Create shopping list "Weekly Groceries"', async () => {
await page.click('a:has-text("Lists")');
await page.waitForURL('**/lists');
await page.click('text=+ New list');
await page.fill('input[placeholder="List name"]', 'Weekly Groceries');
await page.selectOption('select', { label: 'Kroger' });
await page.click('button:has-text("Create")');
await page.waitForSelector('text=Weekly Groceries');
});
await step('Open list and add 5 items', async () => {
await page.click('text=Weekly Groceries');
await page.waitForURL(/\/lists\/\d+/);
for (const item of ['Milk', 'Eggs', 'Bread', 'Chicken breast', 'Broccoli']) {
await page.fill('input[placeholder="Add an item..."]', item);
await page.click('button:has-text("Add")');
await page.waitForSelector(`text=${item}`);
}
});
await step('Check off Milk and Eggs', async () => {
await page.click('button[aria-label="Check Milk"]');
await page.waitForSelector('text=Checked (1)');
await page.click('button[aria-label="Check Eggs"]');
await page.waitForSelector('text=Checked (2)');
});
await step('Uncheck Milk', async () => {
await page.click('button[aria-label="Uncheck Milk"]');
await page.waitForSelector('text=Checked (1)');
});
await step('Remove Broccoli', async () => {
await page.click('button[aria-label="Remove Broccoli"]');
await page.waitForFunction(() => !document.body.textContent.includes('Broccoli'), { timeout: 3000 });
});
await step('Create recipe "Chicken Stir Fry"', async () => {
await page.click('a:has-text("Recipes")');
await page.waitForURL('**/recipes');
await page.click('text=+ New recipe');
await page.waitForURL('**/recipes/new');
await page.fill('input[placeholder="Recipe title"]', 'Chicken Stir Fry');
await page.fill('textarea[placeholder*="description"]', 'Quick weeknight dinner');
const qtyInputs = page.locator('input[placeholder="Qty"]');
const nameInputs = page.locator('input[placeholder="Ingredient name"]');
await qtyInputs.nth(0).fill('2 lbs');
await nameInputs.nth(0).fill('chicken breast');
await page.click('text=+ Add ingredient');
await qtyInputs.nth(1).fill('1 head');
await nameInputs.nth(1).fill('broccoli');
await page.click('text=+ Add ingredient');
await qtyInputs.nth(2).fill('3 tbsp');
await nameInputs.nth(2).fill('soy sauce');
await page.fill('textarea[placeholder*="Instructions"]', '1. Cut chicken\n2. Stir fry\n3. Add sauce');
await page.click('button:has-text("Save Recipe")');
await page.waitForURL(/\/recipes\/\d+/);
});
await step('Add recipe ingredients to shopping list', async () => {
await page.click('button:has-text("Add to list")');
await page.waitForSelector('text=Choose a list');
await page.click('text=Weekly Groceries');
await page.waitForURL(/\/lists\/\d+/);
await page.waitForSelector('text=chicken breast');
});
await step('Verify list has recipe items', async () => {
const content = await page.textContent('body');
for (const item of ['2 lbs chicken breast', '1 head broccoli', '3 tbsp soy sauce']) {
if (!content.includes(item)) throw new Error(`Missing recipe item: ${item}`);
}
});
await step('Navigate back to lists overview', async () => {
await page.click('text=← Back');
await page.waitForURL('**/lists');
});
await step('Sign out', async () => {
await page.click('text=Sign out');
await page.waitForURL('**/login');
});
console.log('\n🎉 All tests passed!');
} catch (e) {
console.error(`\n💥 Test suite stopped at failure: ${e.message}`);
process.exitCode = 1;
} finally {
await new Promise(r => setTimeout(r, 2000));
await browser.close();
}