test: cover kitchen assist meal planning flow
This commit is contained in:
Generated
+66
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.48.3",
|
||||
"version": "0.50.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "oikos",
|
||||
"version": "0.48.3",
|
||||
"version": "0.50.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
@@ -20,6 +20,7 @@
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"puppeteer": "^24.42.0",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
@@ -557,6 +558,22 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
|
||||
@@ -1754,6 +1771,21 @@
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -2639,6 +2671,38 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
|
||||
+3
-1
@@ -31,7 +31,8 @@
|
||||
"test:caldav": "node --experimental-sqlite test-caldav-sync.js",
|
||||
"test:carddav": "node --experimental-sqlite test-carddav.js",
|
||||
"test": "node --experimental-sqlite test-db.js && node --experimental-sqlite test-dashboard.js && node --experimental-sqlite test-tasks.js && node --experimental-sqlite test-multi-assignment.js && node --experimental-sqlite test-shopping.js && node --experimental-sqlite test-meals.js && node --experimental-sqlite test-calendar.js && node --experimental-sqlite test-notes-contacts-budget.js && npm run test:ux-utils && npm run test:modal-utils && npm run test:reminders && npm run test:api && npm run test:ics-parser && npm run test:ics-sub && npm run test:setup && npm run test:kitchen-tabs && npm run test:backup-scheduler && npm run test:caldav && npm run test:carddav",
|
||||
"test:meal-planning": "node --experimental-sqlite test-meal-planning.js"
|
||||
"test:meal-planning": "node --experimental-sqlite test-meal-planning.js",
|
||||
"test:e2e:oikos-flow": "playwright test tests/e2e/oikos-kitchen-assist-flow.spec.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
@@ -50,6 +51,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"puppeteer": "^24.42.0",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const baseURL = process.env.OIKOS_E2E_BASE_URL || 'https://home.friborg.uk';
|
||||
const systemChromium = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| (fs.existsSync('/usr/bin/chromium-browser') ? '/usr/bin/chromium-browser' : undefined)
|
||||
|| (fs.existsSync('/snap/bin/chromium') ? '/snap/bin/chromium' : undefined);
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
timeout: 90_000,
|
||||
expect: { timeout: 15_000 },
|
||||
fullyParallel: false,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
||||
use: {
|
||||
baseURL,
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
ignoreHTTPSErrors: true,
|
||||
actionTimeout: 15_000,
|
||||
navigationTimeout: 30_000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium-desktop',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
launchOptions: systemChromium ? { executablePath: systemChromium } : undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const username = process.env.OIKOS_E2E_USERNAME;
|
||||
const password = process.env.OIKOS_E2E_PASSWORD_FILE
|
||||
? fs.readFileSync(process.env.OIKOS_E2E_PASSWORD_FILE, 'utf8').trim()
|
||||
: process.env.OIKOS_E2E_PASSWORD;
|
||||
|
||||
async function attachDiagnostics(testInfo, diagnostics) {
|
||||
await testInfo.attach('oikos-flow-diagnostics.json', {
|
||||
body: JSON.stringify(diagnostics, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function dismissOnboardingIfPresent(page) {
|
||||
const skip = page.getByRole('button', { name: /^(Skip|Spring over|Senere|Close|Luk)$/i }).first();
|
||||
try {
|
||||
if (await skip.isVisible({ timeout: 2_000 })) await skip.click();
|
||||
} catch {
|
||||
// Onboarding is optional; ignore when it is not present.
|
||||
}
|
||||
}
|
||||
|
||||
async function login(page) {
|
||||
if (!username || !password) {
|
||||
throw new Error('Set OIKOS_E2E_USERNAME and OIKOS_E2E_PASSWORD_FILE (preferred) or OIKOS_E2E_PASSWORD for the live Oikos E2E flow.');
|
||||
}
|
||||
|
||||
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('#username').fill(username);
|
||||
await page.locator('#password').fill(password);
|
||||
|
||||
const loginResponsePromise = page.waitForResponse((res) => res.url().includes('/api/v1/auth/login'));
|
||||
await page.locator('#login-btn').click();
|
||||
const loginResponse = await loginResponsePromise;
|
||||
if (!loginResponse.ok()) throw new Error(`Login failed with HTTP ${loginResponse.status()}`);
|
||||
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 20_000 });
|
||||
await expect(page.locator('#main-content')).toBeVisible();
|
||||
await dismissOnboardingIfPresent(page);
|
||||
}
|
||||
|
||||
test.describe('Oikos Kitchen + Assist meal-planning flow', () => {
|
||||
test('Kitchen opens quickly, Studio is present, and Assist routes meal plans into Studio', async ({ page }, testInfo) => {
|
||||
const diagnostics = {
|
||||
consoleErrors: [],
|
||||
pageErrors: [],
|
||||
failedRequests: [],
|
||||
timings: {},
|
||||
urls: [],
|
||||
};
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (['error', 'warning'].includes(msg.type())) diagnostics.consoleErrors.push(`${msg.type()}: ${msg.text()}`);
|
||||
});
|
||||
page.on('pageerror', (err) => diagnostics.pageErrors.push(err.stack || err.message));
|
||||
page.on('requestfailed', (req) => diagnostics.failedRequests.push(`${req.method()} ${req.url()} :: ${req.failure()?.errorText || 'failed'}`));
|
||||
page.on('framenavigated', (frame) => {
|
||||
if (frame === page.mainFrame()) diagnostics.urls.push(page.url());
|
||||
});
|
||||
|
||||
await page.addInitScript(() => localStorage.setItem('oikos-onboarded', '1'));
|
||||
await login(page);
|
||||
await page.evaluate(() => localStorage.setItem('oikos-onboarded', '1'));
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await dismissOnboardingIfPresent(page);
|
||||
const kitchenNav = page.locator('#sidebar-kitchen-nav, #kitchen-btn').filter({ visible: true }).first();
|
||||
await expect(kitchenNav).toBeVisible();
|
||||
|
||||
const kitchenStart = Date.now();
|
||||
await kitchenNav.click();
|
||||
await page.waitForURL(/\/meals(?:$|[?#])/, { timeout: 15_000 });
|
||||
await expect(page.locator('[data-oikos-meal-plan-studio]')).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('button', { name: /Meal Plan Studio|Generér ugeplan|Regenerér/i }).first()).toBeVisible();
|
||||
diagnostics.timings.kitchenOpenMs = Date.now() - kitchenStart;
|
||||
|
||||
expect(diagnostics.timings.kitchenOpenMs, 'Kitchen menu should open fast enough for real use').toBeLessThan(6_000);
|
||||
|
||||
const studioHost = page.locator('[data-oikos-meal-plan-studio]');
|
||||
await expect(studioHost).toContainText(/Meal Plan Studio/i);
|
||||
|
||||
const assistPrompt = 'Lav en praktisk madplan for næste uge ud fra vores opskrifter.';
|
||||
const assistStart = Date.now();
|
||||
await page.evaluate((prompt) => window.oikos?.openAssist?.({ prompt, expanded: true }), assistPrompt);
|
||||
await expect(page.locator('#oikos-assist-root .assist-panel--open')).toBeVisible();
|
||||
await expect(page.locator('#oikos-assist-root .assist-message--ai').last()).not.toContainText('Tænker…', { timeout: 60_000 });
|
||||
diagnostics.timings.assistMealPlanMs = Date.now() - assistStart;
|
||||
|
||||
const action = page.locator('[data-assist-kitchen-studio]').last();
|
||||
await expect(action, 'Assist meal-plan response should include “Åbn forslag i Køkken”, not only text').toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await action.click();
|
||||
await page.waitForURL(/\/meals(?:$|[?#])/, { timeout: 15_000 });
|
||||
await expect(studioHost.locator('[data-assist-studio-confirm]')).toBeVisible({ timeout: 15_000 });
|
||||
await expect(studioHost.locator('[data-assist-studio-title]')).toHaveCount(7, { timeout: 15_000 });
|
||||
await expect(studioHost).toContainText(/Madlavere & familiepræferencer|Børnenes madplan|Måltidshistorik/i);
|
||||
|
||||
expect(diagnostics.pageErrors, 'No browser page errors during Kitchen/Assist flow').toEqual([]);
|
||||
await attachDiagnostics(testInfo, diagnostics);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user