import { chromium } from 'playwright'; const browser = await chromium.launch({ channel: 'chrome', headless: false, slowMo: 200 }); const contextA = await browser.newContext({ viewport: { width: 390, height: 844 } }); const contextB = await browser.newContext({ viewport: { width: 390, height: 844 } }); const pageA = await contextA.newPage(); const pageB = await contextB.newPage(); const BASE = 'http://localhost:5173'; console.log('⏳ Arrange the two browser windows side by side. Starting in 8 seconds...'); await new Promise(r => setTimeout(r, 8000)); async function step(name, fn) { process.stdout.write(`▶ ${name}... `); try { await fn(); console.log('✅'); } catch (e) { console.log(`❌ ${e.message}`); throw e; } } async function register(page, name) { await page.goto(BASE); await page.waitForURL('**/login', { timeout: 5000 }); await page.click('text=Create account'); await page.fill('input[placeholder="Name"]', name); 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 }); } try { await step('Register User A (Mom)', async () => { await register(pageA, 'Mom'); }); await step('Register User B (Dad)', async () => { await register(pageB, 'Dad'); }); // Mom creates a store await step('Mom creates store "Kroger"', async () => { await pageA.click('a:has-text("Stores")'); await pageA.waitForURL('**/stores'); await pageA.fill('input[placeholder="New store name"]', 'Kroger'); await pageA.click('button:has-text("Add")'); await pageA.waitForSelector('text=Kroger'); }); // Both on lists overview — Mom creates a list, Dad should see it appear via SignalR await step('Mom navigates to Lists', async () => { await pageA.click('a:has-text("Lists")'); await pageA.waitForURL('**/lists'); }); await step('Mom creates list → Dad sees it appear on overview', async () => { await pageA.click('text=+ New list'); await pageA.fill('input[placeholder="List name"]', 'Weekly Groceries'); await pageA.selectOption('select', { label: 'Kroger' }); await pageA.click('button:has-text("Create")'); await pageA.waitForSelector('text=Weekly Groceries'); // Dad should see it appear without navigating away await pageB.waitForSelector('text=Weekly Groceries', { timeout: 5000 }); }); // Both open the list await step('Mom opens the list', async () => { await pageA.click('text=Weekly Groceries'); await pageA.waitForURL(/\/lists\/\d+/); }); await step('Dad opens the same list', async () => { await pageB.click('text=Weekly Groceries'); await pageB.waitForURL(/\/lists\/\d+/); }); // Mom adds items — Dad should see them appear in real-time await step('Mom adds "Milk" → Dad sees it appear', async () => { await pageA.fill('input[placeholder="Add an item..."]', 'Milk'); await pageA.click('button:has-text("Add")'); await pageA.waitForSelector('text=Milk'); await pageB.waitForSelector('text=Milk', { timeout: 5000 }); }); await step('Mom adds "Eggs" → Dad sees it appear', async () => { await pageA.fill('input[placeholder="Add an item..."]', 'Eggs'); await pageA.click('button:has-text("Add")'); await pageA.waitForSelector('text=Eggs'); await pageB.waitForSelector('text=Eggs', { timeout: 5000 }); }); await step('Mom adds "Bread" → Dad sees it appear', async () => { await pageA.fill('input[placeholder="Add an item..."]', 'Bread'); await pageA.click('button:has-text("Add")'); await pageA.waitForSelector('text=Bread'); await pageB.waitForSelector('text=Bread', { timeout: 5000 }); }); // Dad checks off Milk — Mom should see it checked await step('Dad checks "Milk" → Mom sees it checked', async () => { await pageB.click('button[aria-label="Check Milk"]'); await pageB.waitForSelector('text=Checked (1)'); await pageA.waitForSelector('text=Checked (1)', { timeout: 5000 }); }); // Mom checks off Eggs — Dad should see it await step('Mom checks "Eggs" → Dad sees it checked', async () => { await pageA.click('button[aria-label="Check Eggs"]'); await pageA.waitForSelector('text=Checked (2)'); await pageB.waitForSelector('text=Checked (2)', { timeout: 5000 }); }); // Dad unchecks Milk — Mom should see it unchecked await step('Dad unchecks "Milk" → Mom sees it unchecked', async () => { await pageB.click('button[aria-label="Uncheck Milk"]'); await pageB.waitForSelector('text=Checked (1)'); await pageA.waitForSelector('text=Checked (1)', { timeout: 5000 }); }); // Dad adds an item — Mom should see it await step('Dad adds "Butter" → Mom sees it appear', async () => { await pageB.fill('input[placeholder="Add an item..."]', 'Butter'); await pageB.click('button:has-text("Add")'); await pageB.waitForSelector('text=Butter'); await pageA.waitForSelector('text=Butter', { timeout: 5000 }); }); // Mom removes Bread — Dad should see it disappear await step('Mom removes "Bread" → Dad sees it disappear', async () => { await pageA.click('button[aria-label="Remove Bread"]'); await pageA.waitForFunction(() => !document.body.textContent.includes('Bread'), { timeout: 3000 }); await pageB.waitForFunction(() => !document.body.textContent.includes('Bread'), { timeout: 5000 }); }); console.log('\n🎉 All multi-user tests passed! Real-time sync works.'); } catch (e) { console.error(`\n💥 Test failed: ${e.message}`); process.exitCode = 1; } finally { await new Promise(r => setTimeout(r, 2000)); await browser.close(); }