260daaf9d0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
168 lines
12 KiB
JavaScript
168 lines
12 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');
|
|
// viral links: the builder renders a ?ref= link; any page + ?ref=<member> redirects clean and sets the sponsor cookie; an unknown ref sets nothing
|
|
await page.click('.promo-pills [data-promo="viral"]'); await page.waitForTimeout(1200);
|
|
const vl = await page.$eval('#viralLink', e => e.textContent).catch(() => '');
|
|
if (!/^https:\/\/instantadpay\.com\/.*[?&]ref=[a-z0-9_]+$/i.test(vl)) note('bug', L + 'promo/viral', 'builder link not rendered: ' + vl);
|
|
const me = await page.evaluate(() => fetch('/api/me').then(r => r.json()));
|
|
const tok = me.username || me.refCode;
|
|
// served in place (no redirect: Facebook drops the name otherwise), cookie on the response, og:url carries the ref
|
|
const rr = await page.request.get(LOCAL + '/?ref=' + tok + '&x=1', { maxRedirects: 0 });
|
|
const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
|
const rb = await rr.text();
|
|
if (rr.status() !== 200 || !/<title>/.test(rb)) note('bug', L + 'viral/inplace', 'expected the page at the decorated address, got ' + rr.status());
|
|
if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc);
|
|
if (!new RegExp('property="og:url" content="[^"]*[?&]ref=' + tok + '"').test(rb)) note('bug', L + 'viral/og', 'og:url does not carry the ref');
|
|
const ru = await page.request.get(LOCAL + '/?ref=nobody_zz9', { maxRedirects: 0 });
|
|
const su = (ru.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
|
const ub = await ru.text();
|
|
if (ru.status() !== 200 || /iap\.sponsor=/.test(su) || /ref=nobody_zz9/.test(ub)) note('bug', L + 'viral/unknown', 'unknown ref must serve the plain page with no cookie and a clean og:url: ' + ru.status() + ' ' + su);
|
|
const rj = await page.request.get(LOCAL + '/join/' + tok + '?ref=' + tok, { maxRedirects: 0 });
|
|
if (rj.status() !== 200) note('bug', L + 'viral/join', '/join keeps its own ?ref handling, got ' + rj.status());
|
|
console.log('ok viral links: builder + redirect + cookie');
|
|
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);
|