← 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);
  }
})();