#!/usr/bin/env node // ad-regression.mjs — automated ad-flow validation for tubev sites // // Usage: // node ad-regression.mjs [url2] [...] [--full] [--screenshot] // // Default: фрешний context, click body для popunder gesture, потім visits // інших URLs з активним cooldown — перевіряє VAST flow. // // Flags: // --full run full chromium (not headless-shell) — потрібно для VPAID/autoplay // --screenshot save screenshot per URL у /home/w4/playwright-tests/screenshots/ // --json output as JSON (default: human-readable) // // Exit codes: // 0 = всі passed // 1 = хоч один FAIL // // Each URL перевіряється проти 6 checks: // 1. ad-bundle.min.js loaded (200) // 2. popunder SDK loads + asgsl gets timestamps (after click — visit 1 only) // 3. mode у "vast" на visit 2+ // 4. /api/users/ повертає (not empty) // 5. .asg-container injects після click на pjs_play_btn (з data-spot-id) // 6. console — no [ASGB LOADER] errors, no [PAGEERR] import { chromium } from 'playwright'; import { mkdirSync, writeFileSync } from 'fs'; const args = process.argv.slice(2); const FULL = args.includes('--full'); const SCREENSHOT = args.includes('--screenshot'); const JSON_OUT = args.includes('--json'); const URLS = args.filter(a => !a.startsWith('--')); if (URLS.length === 0) { console.error('usage: node ad-regression.mjs [url2] ... [--full] [--screenshot] [--json]'); process.exit(2); } if (SCREENSHOT) mkdirSync('/home/w4/playwright-tests/screenshots', { recursive: true }); const browser = await chromium.launch({ headless: true, args: FULL ? ['--autoplay-policy=no-user-gesture-required'] : [], }); const ctx = await browser.newContext({ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36', viewport: { width: 1280, height: 800 }, acceptDownloads: false, }); const results = []; const checks = (label, pass, detail) => ({ check: label, pass, detail }); const pageCtx = await ctx.newPage(); // ---------- VISIT 1: prime popunder ---------- const url0 = URLS[0]; const id0 = url0.match(/\/v\/box\/(\d+)/)?.[1] || 'unknown'; console.log(`\n${'='.repeat(70)}\nPRIME visit (popunder gesture): /v/box/${id0}`); const reqs0 = []; pageCtx.on('response', r => { if (r.url().includes('a5.g--o.info')) reqs0.push({ status: r.status(), url: r.url() }); }); const consoleErr0 = []; pageCtx.on('console', m => { if (m.type() === 'error') consoleErr0.push(m.text()); }); pageCtx.on('pageerror', e => consoleErr0.push(`[PAGEERR] ${e.message}`)); try { const debugUrl = url0.includes('?') ? `${url0}&debug=1` : `${url0}?debug=1`; await pageCtx.goto(debugUrl, { waitUntil: 'load', timeout: 30000 }); await pageCtx.waitForTimeout(2000); // Click body to fire popunder via gesture await pageCtx.mouse.click(640, 400); await pageCtx.waitForTimeout(3500); const s = await pageCtx.evaluate(() => ({ mode: window._adCtx?.state?.currentMode, _popRr: localStorage.getItem('_popRr'), asgsl: localStorage.getItem('asgsl'), })); const popunderOK = s._popRr && parseInt(s._popRr) > Date.now(); if (!popunderOK) { // Plant manually (headless limitation, doesn't fail run) const future = Date.now() + 2 * 3600 * 1000; await pageCtx.evaluate((t) => { localStorage.setItem('_popRr', String(t)); document.cookie = `_popRr=${t}; max-age=7200; path=/`; const ex = localStorage.getItem('asgsl') || ''; if (ex.includes('514208=') && !ex.includes('global_rr')) { localStorage.setItem('asgsl', ex.replace(/514208=([^|]*)/, `514208=$1,global_rr:${t},n:${t}`)); } else if (!ex.includes('global_rr')) { localStorage.setItem('asgsl', `514208=global_rr:${t},n:${t}` + (ex ? '|' + ex : '')); } }, future); } console.log(` popunder gesture: ${popunderOK ? 'AUTO-FIRED' : 'planted manually (headless limitation)'}`); console.log(` asgsl after: ${(s.asgsl || '').slice(0, 200)}`); } catch (e) { console.log(` PRIME visit error: ${e.message}`); } // ---------- VISIT each URL: full ad-flow check ---------- for (let i = 0; i < URLS.length; i++) { const url = URLS[i]; const id = url.match(/\/v\/box\/(\d+)/)?.[1] || `url-${i+1}`; const debugUrl = url.includes('?') ? `${url}&debug=1` : `${url}?debug=1`; console.log(`\n${'='.repeat(70)}\n[${i+1}/${URLS.length}] /v/box/${id}`); const reqs = []; const consoleErrs = []; const consoleAll = []; const reqHandler = r => { if (r.url().includes('a5.g--o.info')) reqs.push({ status: r.status(), url: r.url(), ts: Date.now() }); }; const conHandler = m => { consoleAll.push(`[${m.type()}] ${m.text()}`); if (m.type() === 'error' || /ASGB LOADER|Response is not okay/.test(m.text())) { consoleErrs.push(m.text()); } }; const errHandler = e => consoleErrs.push(`[PAGEERR] ${e.message}`); const page = await ctx.newPage(); page.on('response', reqHandler); page.on('console', conHandler); page.on('pageerror', errHandler); const urlChecks = []; let initialState = null, finalState = null, vastResp = null; try { await page.goto(debugUrl, { waitUntil: 'load', timeout: 30000 }); await page.waitForTimeout(3500); initialState = await page.evaluate(() => ({ mode: window._adCtx?.state?.currentMode, initialMode: window._adCtx?.state?.initialMode, vastLoaded: window._adCtx?.state?.vastLoaded, _popRr: localStorage.getItem('_popRr'), _vastRr: localStorage.getItem('_vastRr'), asgsl: localStorage.getItem('asgsl'), adConfig: window._adConfig, pjsPlayBtn: !!document.querySelector('#pjs_play_btn'), })); // CHECK 1: ad-bundle loaded const bundleLoaded = reqs.some(r => r.url.includes('ad-bundle') || r.url.includes('/static/js/')); // Actually bundle is on same origin, not a5.g--o.info. Check via _adConfig presence. urlChecks.push(checks('ad-bundle loaded (window._adConfig set)', !!initialState.adConfig, initialState.adConfig ? `popunder=${initialState.adConfig.popunder.spot}, vast=${initialState.adConfig.vast.spot}` : 'window._adConfig undefined')); // CHECK 2: popunder SDK loaded (only on visit 1, but check any) const popSdkLoaded = reqs.some(r => r.url.includes(initialState.adConfig?.popunder?.sdk?.replace('//', ''))); urlChecks.push(checks('popunder SDK loaded (this visit)', popSdkLoaded || initialState.mode === 'vast', popSdkLoaded ? 'SDK fetched' : `mode=${initialState.mode} → SDK not needed (vast mode)`)); // CHECK 3: VAST chain returns (not empty) const vastSpot = initialState.adConfig?.vast?.spot; if (vastSpot) { try { const vastUrl = `https://a5.g--o.info/api/users/${vastSpot}?v2=1&fill=0&url=${encodeURIComponent(url)}&sid=test-${id}`; const r = await fetch(vastUrl, { headers: { Origin: new URL(url).origin, Referer: url, 'User-Agent': 'Mozilla/5.0' }}); const body = await r.text(); const hasAd = //.test(body); const hasWrapper = //.test(body); vastResp = { status: r.status, hasAd, hasInLine, hasWrapper, snippet: body.slice(0, 200) }; urlChecks.push(checks(`VAST endpoint returns (spot ${vastSpot})`, hasAd, hasAd ? `Ad found (Wrapper=${hasWrapper}, InLine=${hasInLine})` : `EMPTY VAST — no-fill response`)); } catch (e) { urlChecks.push(checks(`VAST endpoint check`, false, `fetch error: ${e.message}`)); } } // CHECK 4: mode is "vast" (since popunder cooldown should be active) urlChecks.push(checks(`mode = "vast" (popunder cooldown active)`, initialState.mode === 'vast', `initialMode=${initialState.initialMode}, current=${initialState.mode}, vastLoaded=${initialState.vastLoaded}, _popRr=${initialState._popRr ? 'set' : 'null'}`)); // CHECK 5: click play, observe asg-container injection let containerSeen = false, videoSrcFound = false, videoStarted = false; if (initialState.mode === 'vast' && initialState.pjsPlayBtn) { const btn = await page.$('#pjs_play_btn'); await btn.click({ force: true, timeout: 5000 }).catch(e => {}); for (let j = 0; j < 12; j++) { await page.waitForTimeout(1000); const o = await page.evaluate(() => { const c = document.querySelector('.asg-container'); if (!c) return { c: false }; const v = c.querySelector('video'); return { c: true, spotId: c.dataset.spotId, cls: c.className, videoSrc: v?.currentSrc || null, vTime: v?.currentTime || 0, }; }); if (o.c && !containerSeen) { containerSeen = true; finalState = { ...o }; } if (o.videoSrc && !videoSrcFound) videoSrcFound = true; if (o.vTime > 0 && !videoStarted) { videoStarted = true; break; } } } urlChecks.push(checks(`.asg-container injected on click`, containerSeen, containerSeen ? `data-spot-id=${finalState?.spotId}, videoSrc=${videoSrcFound ? 'set' : 'pending'}` : 'container never injected (12s timeout)')); // CHECK 6: console clean const adsbgErr = consoleErrs.filter(e => /ASGB LOADER|Response is not okay/.test(e)); urlChecks.push(checks(`no ASG console errors`, adsbgErr.length === 0, adsbgErr.length === 0 ? 'clean' : `errors: ${adsbgErr.slice(0, 3).join('; ')}`)); if (SCREENSHOT) { const path = `/home/w4/playwright-tests/screenshots/${id}.png`; await page.screenshot({ path, fullPage: false }); console.log(` 📸 screenshot: ${path}`); } } catch (e) { urlChecks.push(checks('page load', false, `error: ${e.message}`)); } // Print result const passed = urlChecks.filter(c => c.pass).length; const total = urlChecks.length; const allPass = passed === total; console.log(` ${allPass ? '✅' : '❌'} ${passed}/${total} checks pass`); for (const c of urlChecks) { console.log(` ${c.pass ? '✓' : '✗'} ${c.check} — ${c.detail}`); } results.push({ url, id, allPass, checks: urlChecks, vastResp }); await page.close(); } await browser.close(); // Summary console.log(`\n${'='.repeat(70)}\nSUMMARY:`); const totalUrls = results.length; const passedUrls = results.filter(r => r.allPass).length; console.log(` ${passedUrls}/${totalUrls} URLs passed all checks`); results.forEach(r => console.log(` ${r.allPass ? '✅' : '❌'} /v/box/${r.id}`)); if (JSON_OUT) { const reportPath = '/home/w4/playwright-tests/last-report.json'; writeFileSync(reportPath, JSON.stringify({ ts: new Date().toISOString(), results }, null, 2)); console.log(`\n JSON report: ${reportPath}`); } process.exit(passedUrls === totalUrls ? 0 : 1);