149 lines
6.4 KiB
JavaScript
149 lines
6.4 KiB
JavaScript
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.');
|
|
});
|
|
});
|