[object Object]

← back to Qwen38 Viewer

add true E2E (npm run test:e2e): real Chromium drives full chat — 3D scene appears, reply streams, 3D removed on done, zero page/3D errors

598f376fa04c83906b641d5198dfa795904731e0 · 2026-08-19 11:51:49 -0700 · steve

Files touched

Diff

commit 598f376fa04c83906b641d5198dfa795904731e0
Author: steve <steve@designerwallcoverings.com>
Date:   Wed Aug 19 11:51:49 2026 -0700

    add true E2E (npm run test:e2e): real Chromium drives full chat — 3D scene appears, reply streams, 3D removed on done, zero page/3D errors
---
 e2e.js       | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json |  3 +-
 2 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/e2e.js b/e2e.js
new file mode 100644
index 0000000..54d5a45
--- /dev/null
+++ b/e2e.js
@@ -0,0 +1,92 @@
+'use strict';
+/**
+ * True end-to-end browser test: drives a real Chromium through a full chat and
+ * asserts (a) the 3D thinking scene appears, (b) the model's reply streams in,
+ * (c) zero uncaught page errors, (d) the 3D panel is removed when done.
+ *
+ * Hits the model for real (~20s warm, up to ~2min cold), so it's separate from the
+ * fast `npm test` stub suite. Run: npm run test:e2e   (node e2e.js [url])
+ */
+const URL   = process.argv[2] || 'http://127.0.0.1:9821';
+const USER  = process.env.E2E_USER || 'admin';
+const PASS  = process.env.E2E_PASS || 'DW2024!';
+const PROMPT = 'Reply with exactly: e2e ok';
+
+// resolve global/skill playwright without a local install
+function loadPW() {
+  const cands = [
+    '/Users/macstudio3/.npm-global/lib/node_modules/playwright',
+    'playwright',
+    '/Users/macstudio3/.claude/skills/hero-readability-auditor/node_modules/playwright',
+  ];
+  for (const c of cands) { try { return require(c); } catch {} }
+  throw new Error('playwright not found');
+}
+
+let pass = 0, fail = 0;
+const ok = (n, c, x='') => { c ? pass++ : fail++;
+  console.log(`  ${c ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} ${n}${x && !c ? '  → '+x : ''}`); };
+
+(async () => {
+  const { chromium } = loadPW();
+  const browser = await chromium.launch({ headless: true });
+  const ctx = await browser.newContext({ httpCredentials: { username: USER, password: PASS } });
+  const page = await ctx.newPage();
+
+  const pageErrors = [], consoleErrors = [];
+  page.on('pageerror', e => pageErrors.push(String(e)));
+  page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
+
+  let code = 1;
+  try {
+    await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
+    ok('page loads', true);
+
+    // three.js loaded (self-hosted)
+    const hasThree = await page.evaluate(() => !!window.THREE);
+    ok('three.js present (window.THREE)', hasThree);
+
+    // send a message
+    await page.fill('#ta', PROMPT);
+    await page.click('#send');
+
+    // (a) the 3D thinking scene appears with a WebGL canvas
+    await page.waitForSelector('.thinking', { timeout: 15000 });
+    const hasCanvas = await page.$('.thinking .t3d canvas');
+    ok('3D thinking scene appears (canvas)', !!hasCanvas);
+
+    // (b) the reply streams in and finishes (3D panel removed, assistant bubble has text)
+    await page.waitForFunction(() => {
+      const msgs = document.querySelectorAll('.msg.a');
+      const last = msgs[msgs.length - 1];
+      if (!last) return false;
+      if (last.querySelector('.host3d')) return false;         // still generating
+      const b = last.querySelector('.bubble');
+      return b && b.textContent.trim().length > 0;
+    }, { timeout: 190000 });                                     // generous for cold load
+    ok('reply streamed + job completed (3D removed)', true);
+
+    const replyText = await page.evaluate(() => {
+      const m = document.querySelectorAll('.msg.a'); const last = m[m.length-1];
+      return last.querySelector('.bubble').textContent.trim();
+    });
+    ok('reply is non-empty', replyText.length > 0, JSON.stringify(replyText.slice(0,60)));
+    console.log('    model said:', JSON.stringify(replyText.slice(0, 80)));
+
+    // (c) no uncaught page errors during the whole flow
+    ok('zero uncaught page errors', pageErrors.length === 0, pageErrors.join(' | '));
+    // the setState-fallback / WebGL fixes mean no 3D-related console errors either
+    const threeErr = consoleErrors.filter(e => /THREE|setState|WebGL|is not a function/i.test(e));
+    ok('no 3D/console errors', threeErr.length === 0, threeErr.join(' | '));
+
+    code = fail === 0 ? 0 : 1;
+  } catch (e) {
+    ok('E2E flow completed', false, e.message);
+    if (pageErrors.length) console.log('    pageerrors:', pageErrors.join(' | '));
+    code = 1;
+  } finally {
+    await browser.close();
+    console.log(`\n${fail === 0 ? '\x1b[32mE2E PASS\x1b[0m' : '\x1b[31mE2E FAIL\x1b[0m'} — ${pass} passed, ${fail} failed`);
+    process.exit(code);
+  }
+})();
diff --git a/package.json b/package.json
index a94d3f1..2d7e2d2 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
   "main": "server.js",
   "scripts": {
     "start": "node server.js",
-    "test": "node test.js"
+    "test": "node test.js",
+    "test:e2e": "node e2e.js"
   },
   "dependencies": {
     "express": "^4.19.2"

← 6ce074b 5x: two clean sweeps (7/7, 0 defects) — REPORT.md  ·  back to Qwen38 Viewer  ·  chore: v1.1.0 (session close) — already lint/refactor-clean 4956adc →