// 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', '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', '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);