← back to Qwen38 Viewer
e2e.js
93 lines
'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);
}
})();