import { chromium } from 'playwright'; import { createServer } from 'node:http'; import { execFileSync } from 'node:child_process'; import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; const root = process.cwd(); const dogCatRoot = process.env.DOGCAT_ROOT || path.join(root, 'aiAssets', 'DogCat3'); const waitMs = Number(process.env.RUNTIME_WAIT_MS || 9000); const htmlFiles = [ ['Applovin', path.join(dogCatRoot, 'Applovin', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_al.html')], ['Facebook', path.join(dogCatRoot, 'Facebook', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_fb.html')], ['Ironsource', path.join(dogCatRoot, 'Ironsource', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_is.html')], ['Moloco', path.join(dogCatRoot, 'Moloco', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_mo.html')], ['Unity', path.join(dogCatRoot, 'Unity', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_un.html')], ]; const zipFiles = [ ['GoogleAds', path.join(dogCatRoot, 'GoogleAds', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_gg.zip')], ['Mintegral', path.join(dogCatRoot, 'Mintegral', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_mtg.zip')], ['Vungle', path.join(dogCatRoot, 'Vungle', 'wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na_vu.zip')], ]; const mraidStub = ` window.mraid = window.mraid || { getState: function(){ return 'default'; }, isViewable: function(){ return true; }, addEventListener: function(name, cb){ if (name === 'ready') setTimeout(cb, 0); }, removeEventListener: function(){}, open: function(url){ console.log('[stub mraid.open]', url); }, close: function(){ console.log('[stub mraid.close]'); }, supports: function(){ return false; } }; `; const exitApiStub = ` window.ExitApi = window.ExitApi || { exit: function(){ console.log('[stub ExitApi.exit]'); } }; `; async function collectCases(tempDir) { const cases = []; for (const [name, file] of htmlFiles) { if (existsSync(file)) cases.push([name, await readFile(file)]); } for (const [name, zip] of zipFiles) { if (!existsSync(zip)) continue; const out = path.join(tempDir, name); execFileSync('powershell', [ '-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zip.replaceAll("'", "''")}' -DestinationPath '${out.replaceAll("'", "''")}' -Force`, ], { stdio: 'ignore' }); const entries = await readdir(out); const indexName = entries.find((entry) => entry.toLowerCase() === 'index.html'); if (indexName) cases.push([name, await readFile(path.join(out, indexName))]); } return cases; } async function startServer(cases) { const html = new Map(cases.map(([name, body]) => [`/${name}/index.html`, body])); const server = createServer((req, res) => { const url = new URL(req.url || '/', 'http://127.0.0.1'); if (url.pathname.endsWith('/mraid.js')) { res.writeHead(200, { 'content-type': 'application/javascript' }); res.end(mraidStub); return; } if (url.pathname.endsWith('/exitapi.js')) { res.writeHead(200, { 'content-type': 'application/javascript' }); res.end(exitApiStub); return; } const body = html.get(url.pathname); if (body) { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(body); return; } res.writeHead(404, { 'content-type': 'text/plain' }); res.end('not found'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); return { baseUrl: `http://127.0.0.1:${server.address().port}`, close: () => new Promise((resolve) => server.close(resolve)), }; } function simplifyUrl(url) { return url.replace(/^https?:\/\/127\.0\.0\.1:\d+\//, '/'); } async function runCase(browser, baseUrl, name) { const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true }); const issues = []; page.on('pageerror', (error) => { issues.push({ type: 'pageerror', text: error.stack || error.message }); }); page.on('console', (message) => { if (['error', 'assert'].includes(message.type())) { const location = message.location(); const suffix = location.url ? ` @ ${simplifyUrl(location.url)}:${location.lineNumber}:${location.columnNumber}` : ''; issues.push({ type: `console.${message.type()}`, text: `${message.text()}${suffix}` }); } }); page.on('requestfailed', (request) => { issues.push({ type: 'requestfailed', text: `${simplifyUrl(request.url())} :: ${request.failure()?.errorText || 'failed'}`, }); }); page.on('response', (response) => { if (response.status() >= 400) { issues.push({ type: `http.${response.status()}`, text: simplifyUrl(response.url()), }); } }); await page.goto(`${baseUrl}/${name}/index.html`, { waitUntil: 'domcontentloaded', timeout: 20000 }); await page.waitForTimeout(waitMs); const bodyText = await page.locator('body').evaluate((body) => body.innerText.slice(0, 2000)).catch(() => ''); const canvasCount = await page.locator('canvas').count().catch(() => 0); const title = await page.title().catch(() => ''); await page.close(); return { issues, canvasCount, title, bodyText }; } async function main() { const tempDir = await mkdtemp(path.join(tmpdir(), 'dogcat3-playwright-')); let server; let browser; try { const cases = await collectCases(tempDir); server = await startServer(cases); browser = await chromium.launch({ channel: 'msedge', headless: true }); for (const [name] of cases) { const result = await runCase(browser, server.baseUrl, name); console.log(`\n## ${name}`); console.log(`canvas=${result.canvasCount}`); if (result.title) console.log(`title=${result.title}`); if (!result.issues.length) { console.log('No page errors, console errors, failed requests, or HTTP 4xx/5xx responses captured.'); } else { for (const issue of result.issues) { console.log(`- ${issue.type}: ${issue.text.replace(/\s+/g, ' ').slice(0, 700)}`); } } if (result.bodyText && /error|exception|cannot|failed/i.test(result.bodyText)) { console.log(`body_error_text=${result.bodyText.replace(/\s+/g, ' ').slice(0, 700)}`); } } } finally { if (browser) await browser.close(); if (server) await server.close(); await rm(tempDir, { recursive: true, force: true }); } } main().catch((error) => { console.error(error); process.exitCode = 1; });