Files
linkspin/qa/walk.mjs
T

148 lines
9.8 KiB
JavaScript

// InstantAdPay QA harness: site walk.
// node qa/walk.mjs public -> live public pages (no sign-in): errors, failed requests, broken images, dead links, mobile
// node qa/walk.mjs member -> local copy: sign in, every member pane + sub-tab, every admin pane, forms
// node qa/walk.mjs all -> both
// Env: LIVE (default https://instantadpay.com), LOCAL (default http://127.0.0.1:8796), OUT (report dir),
// PW (playwright package dir; default D:/Projects/MarketingAgent/qa-tester/node_modules/playwright)
// Exit code 1 when any [bug] finding remains after noise filtering.
import { pathToFileURL } from 'node:url';
import fs from 'node:fs';
const PW = process.env.PW || 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
const MODE = process.argv[2] || 'all';
const LIVE = process.env.LIVE || 'https://instantadpay.com';
const LOCAL = process.env.LOCAL || 'http://127.0.0.1:8796';
const OUT = process.env.OUT || 'qa/out';
fs.mkdirSync(OUT, { recursive: true });
const findings = [];
const note = (sev, where, what) => findings.push({ sev, where, what });
const NOISE = /walletconnect|reown|web3modal|coingecko|fonts\.|\/api\/feed\/live|\/api\/auth\/logout/;
function watch(page, base) {
const bag = { console: [], failed: [], status: [] };
page.on('pageerror', e => bag.console.push('pageerror: ' + e.message));
page.on('console', m => {
if (m.type() !== 'error') return;
const loc = (m.location() && m.location().url) || '';
if (loc && !loc.startsWith(base)) return; // third-party or framed page, not ours
if (/status of (400|401|404)/.test(m.text())) return; // expected API answers surface as console noise
bag.console.push(m.text());
});
page.on('requestfailed', r => { const u = r.url(); if (u.startsWith(base) && !NOISE.test(u)) bag.failed.push(u + ' ' + (r.failure() && r.failure().errorText)); });
page.on('response', r => { const st = r.status(); const u = r.url(); if (st >= 500 && u.startsWith(base)) bag.status.push(st + ' ' + u); });
return bag;
}
function flush(bag, label) {
for (const c of bag.console) note('bug', label, 'console: ' + c.slice(0, 200));
for (const f of bag.failed) note('bug', label, 'request failed: ' + f.slice(0, 200));
for (const s of bag.status) note('bug', label, 'HTTP ' + s.slice(0, 200));
bag.console.length = bag.failed.length = bag.status.length = 0;
}
async function domChecks(page, label) {
const r = await page.evaluate(() => {
const vis = el => el.offsetParent !== null;
const brokenImgs = [...document.images].filter(i => i.complete && i.naturalWidth === 0 && i.src && vis(i)).map(i => i.src);
const unfilled = [...document.querySelectorAll('body *')].filter(el => el.children.length === 0 && (el.textContent || '').trim() === '…' && vis(el)).length;
const overflow = document.documentElement.scrollWidth > document.documentElement.clientWidth + 2;
return { brokenImgs, unfilled, overflow, title: document.title };
});
if (r.brokenImgs.length) note('bug', label, 'broken images: ' + r.brokenImgs.slice(0, 3).join(', '));
if (r.unfilled) note('warn', label, r.unfilled + ' element(s) still showing the loading ellipsis');
if (r.overflow) note('warn', label, 'page scrolls horizontally');
return r;
}
const hide = page => page.evaluate(() => { document.querySelectorAll('.modal-back,.lgate').forEach(m => m.hidden = true); });
const browser = await chromium.launch();
if (MODE === 'public' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage(); const bag = watch(page, LIVE);
const PUBLIC = ['/', '/ledger', '/contract', '/terms', '/privacy', '/disclaimer', '/wall/martbost', '/join/martbost',
'/join/martbost?v=instant', '/join/martbost?v=adspend', '/join/martbost?v=free', '/join/martbost?v=ledger', '/join/martbost?v=two',
'/admin', '/my', '/shorts', '/nope-404'];
for (const p of PUBLIC) {
const label = 'LIVE ' + p;
try {
const resp = await page.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2500);
const st = resp ? resp.status() : 0;
if (p === '/nope-404') { if (st !== 404) note('warn', label, 'expected 404, got ' + st); }
else if (st >= 400) note('bug', label, 'page HTTP ' + st);
const d = await domChecks(page, label);
const hrefs = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => h.startsWith(location.origin) && !h.includes('#') && !h.includes('/api/')))]);
for (const h of hrefs.slice(0, 40)) {
try { const r = await page.request.head(h, { timeout: 15000 }); if (r.status() >= 400) note('bug', label, 'dead link ' + h + ' -> ' + r.status()); }
catch (e) { note('warn', label, 'link check failed ' + h); }
}
flush(bag, label); console.log('ok', label, '|', d.title);
} catch (e) { note('bug', label, 'navigation failed: ' + e.message.slice(0, 160)); flush(bag, label); }
}
const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true });
const mp = await m.newPage(); const mbag = watch(mp, LIVE);
for (const p of ['/', '/join/martbost?v=instant', '/wall/martbost', '/my']) {
await mp.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => note('bug', 'LIVE mobile ' + p, e.message));
await mp.waitForTimeout(2000); await domChecks(mp, 'LIVE mobile ' + p); flush(mbag, 'LIVE mobile ' + p);
await mp.screenshot({ path: OUT + '/mobile' + p.replace(/[^a-z0-9]+/gi, '-') + '.png' }).catch(() => {});
}
await ctx.close(); await m.close();
}
if (MODE === 'member' || MODE === 'all') {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
const page = await ctx.newPage(); const bag = watch(page, LOCAL);
page.on('dialog', d => d.accept());
const L = 'LOCAL ';
await page.goto(LOCAL + '/my', { waitUntil: 'networkidle' });
await page.fill('#mcEmail', 'qa-walk@example.com'); await page.click('#mcSendBtn');
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
await hide(page); flush(bag, L + 'sign-in');
const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
for (const pn of PANES) {
const label = L + 'my#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label);
const subs = await page.$$('#pane-' + pn + ' .subtabs [data-earn], #pane-' + pn + ' .promo-pills [data-promo]');
for (const s of subs) { try { await s.click(); await page.waitForTimeout(500); } catch (e) {} }
if (subs.length) await domChecks(page, label + ' (sub-tabs)');
flush(bag, label);
await page.screenshot({ path: OUT + '/my-' + pn + '.png' }).catch(() => {});
console.log('ok', label, 'subtabs:', subs.length);
}
await page.click('.bo-menu [data-pane="promo"]'); await page.waitForTimeout(800);
if (!(await page.$$('#promoPosts .promo-block')).length) note('bug', L + 'promo', 'no post cards rendered');
const chat = await page.$('#chatMenuBtn'); if (chat) { await chat.click(); await page.waitForTimeout(800); await domChecks(page, L + 'messages'); flush(bag, L + 'messages'); }
const lo = await page.$('#logoutLink'); if (lo) { await lo.click(); await page.waitForTimeout(1000); }
if (!(await page.$('#authArea:not([hidden])'))) note('bug', L + 'logout', 'auth card not shown after log out');
flush(bag, L + 'logout');
// admin
await page.goto(LOCAL + '/admin', { waitUntil: 'networkidle' });
await page.fill('#adEmail', process.env.ADMIN_EMAIL || 'martybostick@gmail.com'); await page.click('#adSend'); await page.waitForSelector('#adVerify:not([hidden])'); await page.click('#adVerify'); await page.waitForTimeout(1200);
for (const pn of ['overview', 'house', 'campaigns', 'members', 'reports', 'traffic', 'blog', 'releases', 'pnl', 'settings']) {
const label = L + 'admin#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200);
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
if (!vis) note('bug', label, 'pane did not render');
await domChecks(page, label); flush(bag, label);
await page.screenshot({ path: OUT + '/admin-' + pn + '.png' }).catch(() => {});
console.log('ok', label);
}
await page.click('.bo-menu [data-pane="house"]'); await page.waitForTimeout(500);
for (const t of ['banner', 'text', 'login', 'solo', 'video', 'featured', 'visits']) { await page.selectOption('#hType', t); await page.waitForTimeout(120); }
await page.selectOption('#hType', 'text'); await page.click('#hCreate'); await page.waitForTimeout(800);
if (!(await page.$('#hErr:not([hidden])'))) note('warn', L + 'admin house form', 'empty submit showed no validation message');
flush(bag, L + 'admin house form');
await ctx.close();
}
await browser.close();
const bugs = findings.filter(f => f.sev === 'bug'), warns = findings.filter(f => f.sev === 'warn');
const lines = ['===== QA WALK (' + MODE + ') ' + new Date().toISOString() + ' =====', 'bugs: ' + bugs.length + ' | warnings: ' + warns.length,
...findings.map(f => '[' + f.sev + '] ' + f.where + ' :: ' + f.what)];
console.log('\n' + lines.join('\n'));
fs.writeFileSync(OUT + '/walk-report.txt', lines.join('\n') + '\n');
process.exit(bugs.length ? 1 : 0);