timmy-talking-turd/tests/sleek-chat.acceptance.mjs
Timmy 7e11154302
All checks were successful
Quality gates / quality (pull_request) Successful in 1m43s
feat: add private subpage staging slice
2026-08-21 14:12:17 +00:00

121 lines
7.5 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
await mkdir('artifacts', { recursive: true });
const appUrl = process.env.TIMMY_TEST_URL || 'http://127.0.0.1:4173/';
const expectedBasePath = new URL(appUrl).pathname.replace(/\/$/, '') || '/';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
const apiRequests = [];
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', error => errors.push(error.message));
page.on('request', request => { if (new URL(request.url()).pathname.includes('/api/')) apiRequests.push(new URL(request.url()).pathname); });
await page.route('**/api/vision-status', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ enabled: true, profile: 'selfhost', providerReady: true, model: 'SmolVLM2-2.2B-Instruct' }) }));
await page.route('**/api/agent/status', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ enabled: true, configured: true, authenticated: true, mode: 'hermes-agent' }) }));
let chatRequest;
await page.route('**/api/agent/chat', async route => {
chatRequest = route.request().postDataJSON();
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ connected: true, reply: 'Your confirmed logs are mostly Type 4 this week. I can explain the pattern, but I cannot diagnose a cause.' }) });
});
await page.goto(appUrl);
await page.waitForLoadState('networkidle');
assert.equal(await page.evaluate(() => window.__TIMMY_CONFIG__.basePath), expectedBasePath);
assert.equal(await page.locator('.bottom-nav .nav-btn').count(), 3, 'primary navigation must have exactly three destinations');
assert.equal(await page.locator('main .btn-primary:visible').count(), 1, 'home must expose one dominant primary action');
assert.equal(await page.locator('[data-log]:visible').count(), 1, 'manual fallback remains available once without competing styling');
assert.match(await page.locator('main').innerText(), /photo/i);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'home must not overflow horizontally');
if (process.env.TIMMY_EXPECT_STAGING_LABEL) {
assert.match(await page.locator('.staging-label').innerText(), /Staging · daily-test · [0-9a-f]{12}/);
assert.equal(await page.locator('.staging-label').evaluate(node => node.tagName), 'FOOTER');
}
await page.screenshot({ path: 'artifacts/sleek-home-mobile.png', fullPage: true });
await page.locator('[data-view="timmy"]').click();
await page.waitForSelector('#chat-message');
assert.equal(await page.locator('[data-prompt]').count(), 0, 'preset prompt button cluster is removed');
assert.match(await page.locator('.agent-status').innerText(), /Hermes Agent connected/i);
await page.locator('#chat-message').fill('What pattern do you see?');
await page.locator('#send-chat').click();
await page.waitForSelector('.bubble.timmy >> text=mostly Type 4');
assert.equal(chatRequest.message, 'What pattern do you see?');
assert.ok(Array.isArray(chatRequest.ledger));
assert.equal(JSON.stringify(chatRequest).includes('photoDataUrl'), false, 'photos never enter chat context');
const previousRequest = chatRequest;
await page.locator('#chat-message').fill('I barfed');
await page.locator('#send-chat').click();
await page.waitForSelector('.bubble.timmy >> text=Pause and get medical help');
assert.equal(chatRequest, previousRequest, 'urgent language must be intercepted before the Hermes request');
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'chat must not overflow horizontally');
await page.screenshot({ path: 'artifacts/sleek-hermes-chat-mobile.png', fullPage: true });
await page.locator('[data-view="home"]').click();
await page.locator('[data-log]').click();
await page.locator('#next').click();
await page.locator('#next').click();
await page.locator('#save').click();
assert.deepEqual(await page.evaluate(() => Object.keys(localStorage)), [`timmy:${expectedBasePath}:ledger-v1`]);
const serviceWorkerContext = await browser.newContext({ viewport: { width: 390, height: 844 } });
const serviceWorkerPage = await serviceWorkerContext.newPage();
await serviceWorkerPage.goto(new URL('/git', appUrl).toString());
await serviceWorkerPage.evaluate(async () => {
await caches.open('sibling-shared-origin-cache');
await caches.open('timmy-shell-v5:/');
});
await serviceWorkerPage.goto(appUrl);
const registration = await serviceWorkerPage.evaluate(async () => {
const ready = await Promise.race([navigator.serviceWorker.ready, new Promise((_, reject) => setTimeout(() => reject(new Error('service worker timeout')), 5000))]);
return { scope: ready.scope, scriptURL: ready.active?.scriptURL || '', cacheKeys: await caches.keys() };
});
assert.ok(registration.cacheKeys.includes('sibling-shared-origin-cache'), 'Timmy service worker must preserve sibling application caches');
if (expectedBasePath !== '/') assert.ok(registration.cacheKeys.includes('timmy-shell-v5:/'), 'prefixed Timmy must preserve the root Timmy cache');
await serviceWorkerContext.close();
const expectedRoot = expectedBasePath === '/' ? '/' : `${expectedBasePath}/`;
assert.equal(new URL(registration.scope).pathname, expectedRoot);
assert.equal(new URL(registration.scriptURL).pathname, `${expectedRoot}service-worker.js`);
assert.ok(apiRequests.length >= 2);
assert.ok(apiRequests.every(path => path.startsWith(`${expectedRoot}api/`)), `API request escaped prefix: ${apiRequests.join(', ')}`);
if (expectedBasePath === '/') {
const migrationContext = await browser.newContext({ serviceWorkers: 'block' });
await migrationContext.addInitScript(() => {
const entry = id => ({
id,
occurredAt: new Date().toISOString(),
bristolType: 4,
color: 'brown',
urgency: 0,
discomfort: 0,
note: '',
symptoms: {},
});
localStorage.setItem('timmy:/:ledger-v1', JSON.stringify([entry('current-root-entry')]));
localStorage.setItem('timmy-ledger-v1', JSON.stringify([entry('legacy-root-entry')]));
localStorage.setItem('unrelated-sibling-data', 'preserve-me');
});
const migrationPage = await migrationContext.newPage();
await migrationPage.goto(appUrl);
assert.equal(await migrationPage.locator('.glance div').last().locator('strong').innerText(), '1');
assert.match(await migrationPage.evaluate(() => localStorage.getItem('timmy:/:ledger-v1')), /current-root-entry/);
assert.equal(await migrationPage.evaluate(() => localStorage.getItem('timmy-ledger-v1')), null);
await migrationPage.evaluate(() => localStorage.setItem('timmy-ledger-v1', '[{"id":"resurrection-risk"}]'));
await migrationPage.getByRole('button', { name: 'Journal', exact: true }).click();
await migrationPage.locator('[data-view="privacy"]').click();
migrationPage.once('dialog', dialog => dialog.accept());
await migrationPage.locator('#delete-all').click();
assert.equal(await migrationPage.evaluate(() => localStorage.getItem('timmy:/:ledger-v1')), null);
assert.equal(await migrationPage.evaluate(() => localStorage.getItem('timmy-ledger-v1')), null);
assert.equal(await migrationPage.evaluate(() => localStorage.getItem('unrelated-sibling-data')), 'preserve-me');
await migrationContext.close();
}
assert.deepEqual(errors, []);
await browser.close();
console.log('Sleek shell + Hermes chat mobile acceptance passed.');