timmy-talking-turd/tests/ui.acceptance.mjs
Timmy 6f73b8551c
All checks were successful
Quality gates / quality (pull_request) Successful in 4m14s
fix: close second hostile-review round on ledger portability
- collision-safe ID repair: duplicate ids inside stored data are repaired
  deterministically (first keeps id, twins get id#2, id#3, ... scanning past
  owned suffixes); every distinct local record survives, never dropped or
  silently merged; hostile id types (Symbol/BigInt/objects) repair onto
  fresh deterministic ids instead of throwing
- transactional import: parse+merge into a candidate ledger, persist first,
  then commit memory; quota/error rolls back in-memory state and localStorage
  together with explicit user feedback; total 16MiB portability budget
  enforced before mutation on export, import (post-migration expansion), and
  storage writes
- strict Timmy legacy contract for bare top-level arrays: nonempty array of
  plain rows each carrying a nonempty string id and integer Bristol 1-7;
  arbitrary unrelated arrays are rejected wholesale - no invented medical
  defaults from foreign JSON
- canonical raster photo validation: strict JPEG/PNG/WebP grammar, canonical
  base64 (linear scan, no regex on multi-MB strings), atob round-trip decode,
  declared-format magic bytes, 32B-4MiB decoded bounds; mislabeled SVG/HTML
  and noncanonical tiny junk are stripped while genuine photos survive
  byte-for-byte
- migrateStoredLedger: localStorage is validated and migrated before render;
  invalid dates become safe ISO timestamps, duplicate ids repaired, junk rows
  dropped (never fabricated into default records); healthy storage is
  byte-stable and never rewritten
- sanitizeEntry absorbs Symbol/BigInt/hostile dates/throwing toString,
  valueOf, getTime, toJSON without throwing; results stay serializable
- browser regression suite: quota rollback, pre-render migration, array
  rejection, photo contract, duplicate-ID preservation in the real app flow
- staging-health startup-rejection budget anchored to measured server
  cold-start instead of a fixed 800ms (fixes load-sensitive flake)
2026-08-22 23:47:25 +00:00

50 lines
2.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 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 = [];
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', error => errors.push(error.message));
const appUrl = process.env.TIMMY_TEST_URL || 'http://127.0.0.1:4173';
await page.goto(appUrl, { waitUntil: 'networkidle' });
await page.evaluate(() => localStorage.clear());
await page.reload({ waitUntil: 'networkidle' });
assert.match(await page.locator('h1').innerText(), /Log it/i);
assert.equal(await page.locator('.bottom-nav .nav-btn').count(), 3);
assert.equal(await page.locator('[data-scan]').first().isVisible(), true);
assert.equal(await page.locator('[data-log]').first().isVisible(), true);
await page.screenshot({ path: 'artifacts/home-mobile.png', fullPage: false });
await page.locator('[data-log]').first().click();
await page.locator('[data-type="6"]').click();
await page.locator('#next').click();
await page.locator('#color').selectOption('brown');
await page.locator('#urgency').fill('3');
await page.locator('#note').fill('After lunch');
await page.locator('#next').click();
await page.locator('[data-symptom="blood"]').check();
assert.equal(await page.getByText('Pause and get medical help.').isVisible(), true);
await page.screenshot({ path: 'artifacts/red-flag-mobile.png', fullPage: true });
await page.locator('#save').click();
assert.equal(await page.getByText('1', { exact: true }).first().isVisible(), true);
await page.waitForTimeout(2600);
await page.locator('[data-view="timmy"]').last().click();
await page.locator('#chat-message').fill('Can I eat Taco Bell?');
await page.locator('#send-chat').click();
const chatText = await page.locator('#chat').innerText();
assert.match(chatText, /pause and get medical help/i);
assert.doesNotMatch(chatText, /Taco Bell is safe|cannot clear a food or restaurant/i);
await page.waitForTimeout(500);
assert.equal(await page.evaluate(() => window.scrollY), 0, 'reply should not push the page header/navigation out of frame');
await page.screenshot({ path: 'artifacts/timmy-chat-mobile.png', fullPage: false });
assert.deepEqual(errors, []);
await browser.close();
console.log('PASS mobile home → log → red-flag → save → Timmy boundary');