const { test, expect } = require('@playwright/test'); const png = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=', 'base64', ); async function mockModels(page) { await page.route('**/api/models', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ defaultModel: 'claude-haiku-4.5', models: [ { id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5' }, ], }), }); }); await page.route('**/api/models/search', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ count: 2, models: [ { id: 'claude-sonnet-4.6', name: 'Claude Sonnet 4.6' }, { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' }, ], }), }); }); await page.route('**/api/models/add', async (route) => { const body = route.request().postDataJSON(); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, defaultModel: 'claude-haiku-4.5', models: [ { id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5' }, { id: body.id, name: body.name || body.id }, ], }), }); }); } async function login(page) { await mockModels(page); await page.goto('/'); await expect(page).toHaveURL(/\/login$/); await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible(); await page.locator('#password').fill('test-password'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); } test('requires authentication before loading the app', async ({ page, request }) => { await page.goto('/'); await expect(page).toHaveURL(/\/login$/); await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible(); const response = await request.post('/api/chat', { data: {} }); expect(response.status()).toBe(401); }); test.describe('authenticated app', () => { test.beforeEach(async ({ page }) => { await login(page); await page.evaluate(() => localStorage.clear()); await page.reload(); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); }); test('uses searchable page navigation and validates missing meal input', async ({ page }) => { await expect(page.getByRole('navigation', { name: 'Main menu' })).toBeVisible(); await expect(page.locator('#todayCalories')).toHaveText('0 kcal'); await page.locator('#menu-search').fill('log'); await expect(page.getByRole('button', { name: 'Log meal' })).toBeVisible(); await page.getByRole('button', { name: 'Log meal' }).click(); await page.getByRole('button', { name: 'Analyze and save' }).click(); await expect(page.locator('#status')).toContainText('Describe the meal or upload an image.'); }); test('adds curated models in settings, backfills a meal, edits, trashes, and restores', async ({ page }) => { let calls = 0; await page.route('**/api/chat', async (route) => { calls += 1; const content = calls === 1 ? '{"foods":["salmon","rice","broccoli","berries"],"calories":620,"proteinGrams":42,"carbsGrams":58,"fatGrams":22,"fruitServings":0.5,"vegetableServings":1.5}' : '{"mealName":"Salmon rice bowl","calories":640,"proteinGrams":44,"carbsGrams":60,"fatGrams":23,"fruitServings":0.5,"vegetableServings":1.5,"foodGroups":"fish, grains, fruit, vegetables","notes":"Estimated from image and portion note."}'; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ choices: [{ message: { content } }] }), }); }); await page.getByRole('banner').getByRole('button', { name: 'Settings' }).click(); await page.locator('#modelSearch').fill('gemini'); await page.getByRole('button', { name: 'Search' }).click(); await expect(page.getByText('Gemini 2.5 Flash')).toBeVisible(); await page.getByRole('button', { name: '+' }).last().click(); await page.locator('#visionModel').selectOption('gemini-2.5-flash'); await page.locator('#taskModel').selectOption('gemini-2.5-flash'); await page.getByRole('button', { name: 'Save selected models' }).click(); await expect(page.locator('#modelStatus')).toContainText('Models saved.'); await page.getByRole('button', { name: 'Log meal' }).click(); await page.locator('#mealDate').fill('2026-05-18'); await page.locator('#mealTime').fill('08:15'); await page.locator('#mealType').selectOption('Breakfast'); await page.locator('#description').fill('salmon rice bowl with broccoli and berries'); await page.locator('#measure').fill('one dinner bowl'); await page.locator('#image').setInputFiles({ name: 'meal.png', mimeType: 'image/png', buffer: png }); await expect(page.locator('#preview')).toBeVisible(); await page.getByRole('button', { name: 'Analyze and save' }).click(); await expect(page.locator('#status')).toContainText('Saved.'); await page.locator('nav').getByRole('button').filter({ hasText: 'Diary' }).click(); await expect(page.locator('.entry')).toContainText('Salmon rice bowl'); await expect(page.locator('.entry')).toContainText('2026-05-18 08:15 ยท Breakfast'); await page.getByRole('button', { name: 'Edit' }).click(); await expect(page.getByRole('heading', { name: 'Edit meal' })).toBeVisible(); await page.getByLabel('Calories').fill('610'); await page.getByRole('button', { name: 'Save edits' }).click(); await page.locator('nav').getByRole('button').filter({ hasText: 'Analytics' }).click(); await expect(page.locator('#calorieChart')).toBeVisible(); await expect(page.locator('#produceChart')).toBeVisible(); await page.locator('nav').getByRole('button').filter({ hasText: 'Diary' }).click(); await page.getByRole('checkbox').check(); await page.getByRole('button', { name: 'Move selected to trash' }).click(); await expect(page.locator('#entries')).toContainText('No meals match the current filters.'); await page.locator('nav').getByRole('button').filter({ hasText: 'Trash' }).click(); await expect(page.locator('#trashEntries')).toContainText('Salmon rice bowl'); await page.getByRole('button', { name: 'Restore' }).click(); await expect(page.locator('#trashEntries')).toContainText('Trash is empty.'); }); });