← 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 →