← back to Stock Vshape Viewer
test/panel.spec.mjs
124 lines
// Panel behavior spec — opens the collapsed #left hamburger and exercises the controls
// the /3x click-through can't reach. Run: node test/panel.spec.mjs [url]
// Needs the server running (default http://localhost:9822/). Uses global Playwright.
import { createRequire } from 'module';
import os from 'os';
import path from 'path';
const require = createRequire(import.meta.url);
const URL = process.argv[2] || 'http://localhost:9822/';
const CREDS = { username: 'admin', password: 'DW2024!' };
// resolve global Playwright
let pw;
for (const p of [
path.join(os.homedir(), '.npm-global/lib/node_modules/playwright'),
'/usr/local/lib/node_modules/playwright', 'playwright',
]) { try { pw = require(p); break; } catch {} }
if (!pw) { console.error('Playwright not found (npm i -g playwright)'); process.exit(2); }
async function launch() {
for (const opt of [{ channel: 'chrome' }, {}, { executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' }]) {
try { return await pw.chromium.launch(opt); } catch {}
}
throw new Error('could not launch a chromium/chrome');
}
let pass = 0, fail = 0;
const ok = (name, cond, detail = '') => { (cond ? (pass++, console.log(' ✅', name, detail)) : (fail++, console.log(' ❌', name, detail))); };
const run = async () => {
const browser = await launch();
const ctx = await browser.newContext({ httpCredentials: CREDS, viewport: { width: 1440, height: 1000 } });
await ctx.addInitScript(() => { try { localStorage.clear(); } catch {} }); // deterministic seed
const page = await ctx.newPage();
const jsErrors = [];
page.on('pageerror', e => jsErrors.push(String(e)));
page.on('console', m => { if (m.type() === 'error' && !/favicon/.test(m.text())) jsErrors.push(m.text()); });
console.log(`\n/panel-spec → ${URL}\n`);
await page.goto(URL);
await page.waitForFunction(() => document.querySelectorAll('#wl .item').length >= 6 && /live/.test(document.getElementById('live-txt').textContent), { timeout: 25000 });
ok('live data loads (seed watchlist)', true);
// open the hamburger panel
await page.click('#burger');
ok('panel opens', await page.evaluate(() => document.getElementById('left').classList.contains('open')));
// --- SCAN ---
const wlBefore = await page.evaluate(() => document.querySelectorAll('#wl .item').length);
await page.evaluate(() => { const s = document.getElementById('scan-max'); s.value = 120; s.dispatchEvent(new Event('input', { bubbles: true })); });
await page.click('#scan-btn');
await page.waitForFunction(() => document.getElementById('scan-prog').textContent.includes('loaded top'), { timeout: 45000 });
const scan = await page.evaluate(() => ({
matches: +((document.getElementById('scan-prog').textContent.match(/(\d+) matches/) || [])[1] || 0),
results: document.querySelectorAll('#scan-results .r').length,
wl: document.querySelectorAll('#wl .item').length,
}));
ok('scan → finds matches', scan.matches > 0, `${scan.matches} matches`);
ok('scan → renders results list', scan.results > 0, `${scan.results} rows`);
ok('scan → auto-loads board', scan.wl > wlBefore, `${wlBefore}→${scan.wl}`);
// --- ADD ---
await page.fill('#add-tk', 'AAPL');
await page.click('#add-btn');
await page.waitForFunction(() => [...document.querySelectorAll('#wl .tk')].some(e => e.textContent === 'AAPL'), { timeout: 15000 });
ok('add ticker → AAPL on watchlist', true);
// --- RESET WATCHLIST ---
await page.click('#reset-watchlist');
await page.waitForFunction(() => document.querySelectorAll('#wl .item').length === 6, { timeout: 15000 });
ok('reset watchlist → back to seed 6', true);
// --- RESET ALL ---
await page.evaluate(() => { const m = document.getElementById('f-minDip'); m.value = 25; m.dispatchEvent(new Event('input', { bubbles: true })); });
await page.click('#reset-all');
await page.waitForTimeout(600);
const ra = await page.evaluate(() => ({ minDip: document.getElementById('v-minDip').textContent, wl: document.querySelectorAll('#wl .item').length }));
ok('reset all → minDip default 10%', ra.minDip === '10%', ra.minDip);
ok('reset all → seed watchlist', ra.wl === 6, String(ra.wl));
// --- OPTIONS ---
await page.evaluate(() => { const n = [...document.querySelectorAll('#wl .item')].find(it => it.querySelector('.tk')?.textContent === 'NFLX'); n && n.click(); });
await page.waitForSelector('#detail.show', { timeout: 8000 });
await page.click('#d-opttoggle');
await page.waitForFunction(() => { const b = document.getElementById('d-opts'); return b && (b.querySelector('table') || /No contracts|no options/i.test(b.textContent)); }, { timeout: 20000 });
const opt = await page.evaluate(() => ({ hasTable: !!document.querySelector('#d-opts table'), rows: document.querySelectorAll('#d-opts table tr').length }));
ok('options → chain loads', opt.hasTable || true, opt.hasTable ? `${opt.rows} rows` : 'no qualifying contracts (valid)');
await page.evaluate(() => { const p = [...document.querySelectorAll('#seg-optType button')].find(b => b.dataset.v === 'puts'); p.click(); });
await page.waitForTimeout(1500);
ok('options → type toggle (Puts)', await page.evaluate(() => document.querySelector('#seg-optType button[data-v="puts"]').classList.contains('on')));
// --- PRESET ---
await page.evaluate(() => document.querySelector('#presets .p[data-i="0"]').click());
await page.waitForTimeout(600);
ok('preset → Deep V-Recovery sets dip≥20%', await page.evaluate(() => document.getElementById('crit-dip').textContent === '≥20%'));
// --- BRUSH (custom range) ---
await page.evaluate(() => { const n = [...document.querySelectorAll('#wl .item')].find(it => it.querySelector('.tk')?.textContent === 'NFLX'); n && n.click(); });
await page.waitForSelector('#detail.show', { timeout: 8000 });
const box = await page.evaluate(() => { const r = document.getElementById('d-brush').getBoundingClientRect(); return { x: r.left, y: r.top, w: r.width, h: r.height }; });
await page.mouse.move(box.x + box.w * 0.5, box.y + box.h / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.w * 0.85, box.y + box.h / 2, { steps: 6 });
await page.mouse.up();
await page.waitForTimeout(500);
ok('brush → sets custom range', await page.evaluate(() => /Custom range/.test(document.getElementById('d-vnote').textContent)));
// --- SCORE BACKTEST ---
await page.evaluate(() => document.getElementById('d-bttoggle').click());
await page.waitForTimeout(2500);
const bt = await page.evaluate(() => document.getElementById('d-backtest').textContent);
ok('backtest panel responds (building or data, no error)', /names|building|forward return|Read|correlation/.test(bt) && !/error/i.test(bt), bt.slice(0, 40));
// --- console cleanliness ---
ok('no JS/console errors', jsErrors.length === 0, jsErrors.slice(0, 3).join(' | '));
await browser.close();
console.log(`\n ${pass} passed · ${fail} failed\n`);
process.exit(fail ? 1 : 0);
};
run().catch(e => { console.error('spec crashed:', e); process.exit(1); });