624 lines
24 KiB
JavaScript
624 lines
24 KiB
JavaScript
const puppeteer = require("puppeteer-core");
|
|
const fs = require("fs");
|
|
|
|
(async () => {
|
|
const t0 = Date.now();
|
|
const ts = () => new Date().toISOString() + " +" + (Date.now() - t0) + "ms";
|
|
const log = (...args) => console.log(ts(), ...args);
|
|
log("Puppeteer test starting");
|
|
let browser;
|
|
// Global timeout (ms) for the whole test run to avoid infinite log floods.
|
|
const MAX_TEST_MS = parseInt(process.env.TEST_MAX_MS || "60000", 10);
|
|
let _globalTimeout = null;
|
|
let _timedOut = false;
|
|
const startGlobalTimeout = () => {
|
|
if (_globalTimeout) return;
|
|
_globalTimeout = setTimeout(async () => {
|
|
_timedOut = true;
|
|
try {
|
|
log(`Global timeout of ${MAX_TEST_MS}ms reached — aborting test`);
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
log('Browser closed due to global timeout');
|
|
} catch (e) {
|
|
log('Error while closing browser on timeout:', e && e.message ? e.message : e);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// swallow
|
|
}
|
|
// Exit with distinct code so CI can tell a timeout occurred.
|
|
process.exit(124);
|
|
}, MAX_TEST_MS);
|
|
};
|
|
const clearGlobalTimeout = () => {
|
|
try {
|
|
if (_globalTimeout) clearTimeout(_globalTimeout);
|
|
} catch (e) {}
|
|
_globalTimeout = null;
|
|
};
|
|
// Start the global timeout immediately so the whole test run (including
|
|
// browser launch) is bounded by TEST_MAX_MS. This makes the timeout work
|
|
// even when env vars are not propagated the way the caller expects.
|
|
startGlobalTimeout();
|
|
try {
|
|
// Try to use a system Chrome/Chromium binary if present. This lets
|
|
// developers skip downloading Chromium during `npm install` by setting
|
|
// PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 and supplying CHROME_PATH.
|
|
const possiblePaths = [];
|
|
if (process.env.CHROME_PATH) possiblePaths.push(process.env.CHROME_PATH);
|
|
possiblePaths.push(
|
|
"/usr/bin/google-chrome-stable",
|
|
"/usr/bin/google-chrome",
|
|
"/usr/bin/chromium-browser",
|
|
"/usr/bin/chromium"
|
|
);
|
|
let launchOptions = {
|
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
ignoreHTTPSErrors: true,
|
|
};
|
|
for (const p of possiblePaths) {
|
|
try {
|
|
const stat = require("fs").statSync(p);
|
|
if (stat && stat.isFile()) {
|
|
log("Found system Chrome at", p, "— will use it");
|
|
launchOptions.executablePath = p;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
log(
|
|
"Launching Puppeteer with options:",
|
|
Object.assign({}, launchOptions, {
|
|
executablePath: launchOptions.executablePath
|
|
? launchOptions.executablePath
|
|
: "<auto>",
|
|
})
|
|
);
|
|
const tLaunch = Date.now();
|
|
browser = await puppeteer.launch(launchOptions);
|
|
log("Browser launched (took", Date.now() - tLaunch + "ms)");
|
|
// Start the global timeout after browser is launched so we can reliably
|
|
// close the browser if we need to abort.
|
|
startGlobalTimeout();
|
|
const page = await browser.newPage();
|
|
log("New page created");
|
|
|
|
// Intercept WebSocket.prototype.send as early as possible so we can
|
|
// capture outgoing messages from the client (helps verify the name
|
|
// submit handler triggered). Use evaluateOnNewDocument so interception
|
|
// is in place before any page script runs.
|
|
try {
|
|
await page.evaluateOnNewDocument(() => {
|
|
try {
|
|
const orig = WebSocket.prototype.send;
|
|
WebSocket.prototype.send = function (data) {
|
|
try {
|
|
window.__wsSends = window.__wsSends || [];
|
|
let d = data;
|
|
// Handle ArrayBuffer or typed arrays by attempting to decode
|
|
if (d instanceof ArrayBuffer || ArrayBuffer.isView(d)) {
|
|
try {
|
|
d = new TextDecoder().decode(d);
|
|
} catch (e) {
|
|
d = '' + d;
|
|
}
|
|
}
|
|
const entry = { ts: Date.now(), data: typeof d === 'string' ? d : (d && d.toString ? d.toString() : JSON.stringify(d)) };
|
|
window.__wsSends.push(entry);
|
|
// Emit a console log so the host log captures the message body
|
|
try { console.log('WS_SEND_INTERCEPT', entry.data); } catch (e) {}
|
|
} catch (e) {
|
|
// swallow
|
|
}
|
|
return orig.apply(this, arguments);
|
|
};
|
|
} catch (e) {
|
|
// swallow
|
|
}
|
|
});
|
|
log('Installed WebSocket send interceptor (evaluateOnNewDocument)');
|
|
} catch (e) {
|
|
log('Could not install WS interceptor:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
// forward page console messages to our logs (helpful to see runtime errors)
|
|
page.on('console', msg => {
|
|
try {
|
|
const args = msg.args ? msg.args.map(a => a.toString()).join(' ') : msg.text();
|
|
log('PAGE_CONSOLE', msg.type(), args);
|
|
} catch (e) {
|
|
log('PAGE_CONSOLE', msg.type(), msg.text());
|
|
}
|
|
});
|
|
page.on('requestfailed', req => {
|
|
try { log('REQUEST_FAILED', req.url(), req.failure && req.failure().errorText); } catch (e) { log('REQUEST_FAILED', req.url()); }
|
|
});
|
|
|
|
// track simple network counts so we can see if navigation triggers many requests
|
|
let reqCount = 0,
|
|
resCount = 0;
|
|
page.on("request", (r) => {
|
|
reqCount++;
|
|
});
|
|
page.on("response", (r) => {
|
|
resCount++;
|
|
});
|
|
|
|
// small helper to replace the deprecated page.waitForTimeout
|
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
|
|
// Navigate to the dev client; allow overriding via CLI or env
|
|
const url =
|
|
process.argv[2] ||
|
|
process.env.TEST_URL ||
|
|
"https://localhost:3001/ketr.ketran/";
|
|
log("Navigating to", url);
|
|
try {
|
|
log("Calling page.goto (domcontentloaded, 10s timeout)");
|
|
const tNavStart = Date.now();
|
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
|
|
log(
|
|
"Page load (domcontentloaded) finished (took",
|
|
Date.now() - tNavStart + "ms)",
|
|
"requests=",
|
|
reqCount,
|
|
"responses=",
|
|
resCount
|
|
);
|
|
|
|
// Diagnostic snapshot: save the full HTML and a screenshot immediately
|
|
// after domcontentloaded so we can inspect the initial page state even
|
|
// when the inputs appear much later.
|
|
try {
|
|
const domContent = await page.content();
|
|
try {
|
|
fs.writeFileSync('/workspace/tmp-domcontent.html', domContent);
|
|
log('Saved domcontent HTML to /workspace/tmp-domcontent.html (length', domContent.length + ')');
|
|
} catch (e) {
|
|
log('Failed to write domcontent HTML:', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
log('Could not read page content at domcontentloaded:', e && e.message ? e.message : e);
|
|
}
|
|
try {
|
|
const shotNow = '/workspace/tmp-domcontent.png';
|
|
await page.screenshot({ path: shotNow, fullPage: true });
|
|
log('Saved domcontent screenshot to', shotNow);
|
|
} catch (e) {
|
|
log('Failed to save domcontent screenshot:', e && e.message ? e.message : e);
|
|
}
|
|
try {
|
|
const immediateInputs = await page.evaluate(() => document.querySelectorAll('input').length);
|
|
log('Immediate input count at domcontentloaded =', immediateInputs);
|
|
} catch (e) {
|
|
log('Could not evaluate immediate input count:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
// Install a MutationObserver inside the page to record the first time any
|
|
// input element appears. We store the epoch ms in window.__inputsFirstSeen
|
|
// and also emit a console log so the test logs capture the exact instant.
|
|
try {
|
|
await page.evaluate(() => {
|
|
try {
|
|
window.__inputsFirstSeen = null;
|
|
const check = () => {
|
|
try {
|
|
const count = document.querySelectorAll('input').length;
|
|
if (count && window.__inputsFirstSeen === null) {
|
|
window.__inputsFirstSeen = Date.now();
|
|
// eslint-disable-next-line no-console
|
|
console.log('INPUTS_APPEARED', window.__inputsFirstSeen, 'count=', count);
|
|
if (observer) observer.disconnect();
|
|
}
|
|
} catch (e) {
|
|
// swallow
|
|
}
|
|
};
|
|
const observer = new MutationObserver(check);
|
|
observer.observe(document.body || document.documentElement, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
});
|
|
// Run an initial check in case inputs are already present
|
|
check();
|
|
} catch (e) {
|
|
// ignore page-side errors
|
|
}
|
|
});
|
|
log('MutationObserver probe installed to detect first input appearance');
|
|
} catch (e) {
|
|
log('Could not install MutationObserver probe:', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load page:", e && e.message ? e.message : e);
|
|
// Print a short sniff of the response HTML if available
|
|
try {
|
|
const content = await page.content();
|
|
console.log("Page content snippet:\n", content.slice(0, 1000));
|
|
} catch (err) {
|
|
console.warn(
|
|
"Could not read page content:",
|
|
err && err.message ? err.message : err
|
|
);
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
// Fast-path: wait for a visible input quickly. If that fails, fall back to polling.
|
|
let inputsFound = 0;
|
|
try {
|
|
log('Fast-wait for visible input (5s)');
|
|
await page.waitForSelector('input', { visible: true, timeout: 5000 });
|
|
inputsFound = await page.evaluate(() => document.querySelectorAll('input').length);
|
|
log('Fast-wait success, inputsFound=', inputsFound);
|
|
} catch (e) {
|
|
log('Fast-wait failed (no visible input within 5s), falling back to polling');
|
|
// poll for inputs so we know exactly when they appear rather than sleeping blindly
|
|
const pollStart = Date.now();
|
|
for (let i = 0; i < 80; i++) { // 80 * 250ms = 20s fallback
|
|
try {
|
|
inputsFound = await page.evaluate(() => document.querySelectorAll('input').length);
|
|
} catch (err) {
|
|
log('evaluate error while polling inputs:', err && err.message ? err.message : err);
|
|
}
|
|
// Abort the polling if the global timeout has already fired or is about to.
|
|
if (_timedOut) {
|
|
log('Aborting input polling because global timeout flag is set');
|
|
break;
|
|
}
|
|
if (Date.now() - t0 > MAX_TEST_MS - 2000) {
|
|
log('Approaching global timeout while polling inputs — aborting polling loop');
|
|
break;
|
|
}
|
|
if (inputsFound && inputsFound > 0) {
|
|
log('Inputs appeared after', (Date.now() - pollStart) + 'ms; count=', inputsFound);
|
|
break;
|
|
}
|
|
if (i % 20 === 0) log('still polling for inputs...', i, 'iterations, elapsed', (Date.now() - pollStart) + 'ms');
|
|
await sleep(250);
|
|
}
|
|
if (!inputsFound) log('No inputs found after fallback polling (~20s)');
|
|
}
|
|
|
|
// If the app shows an "Enter your name" prompt, robustly try to fill it
|
|
try {
|
|
log('Attempting to detect name input by placeholder/aria/label/id or first visible input');
|
|
const candidates = [
|
|
"input[placeholder*='Enter your name']",
|
|
"input[placeholder*='enter your name']",
|
|
"input[aria-label*='name']",
|
|
"input[name='name']",
|
|
"input[id*='name']",
|
|
".nameInput input",
|
|
// label-based XPath (will be handled specially below)
|
|
"xpath://label[contains(normalize-space(.), 'Enter your name')]/following::input[1]",
|
|
"xpath://*[contains(normalize-space(.), 'Enter your name')]/following::input[1]",
|
|
"input"
|
|
];
|
|
let filled = false;
|
|
for (const sel of candidates) {
|
|
try {
|
|
let el = null;
|
|
if (sel.startsWith('xpath:')) {
|
|
const path = sel.substring(6);
|
|
const nodes = await page.$x(path);
|
|
if (nodes && nodes.length) el = nodes[0];
|
|
} else {
|
|
el = await page.$(sel);
|
|
}
|
|
if (el) {
|
|
await el.focus();
|
|
const tTypeStart = Date.now();
|
|
await page.keyboard.type('Automaton', { delay: 50 });
|
|
log('Typed name via selector', sel, '(took', Date.now() - tTypeStart + 'ms)');
|
|
// Press Enter to submit if the UI responds to it
|
|
await page.keyboard.press('Enter');
|
|
filled = true;
|
|
await sleep(500);
|
|
break;
|
|
}
|
|
} catch (inner) {
|
|
// ignore selector-specific failures
|
|
}
|
|
}
|
|
|
|
if (!filled) {
|
|
// fallback: find the first visible input and type there
|
|
const inputs = await page.$$('input');
|
|
for (const input of inputs) {
|
|
try {
|
|
const box = await input.boundingBox();
|
|
if (box) {
|
|
await input.focus();
|
|
const tTypeStart = Date.now();
|
|
await page.keyboard.type('Automaton', { delay: 50 });
|
|
await page.keyboard.press('Enter');
|
|
log('Typed name into first visible input (took', Date.now() - tTypeStart + 'ms)');
|
|
filled = true;
|
|
await sleep(500);
|
|
break;
|
|
}
|
|
} catch (inner) {}
|
|
}
|
|
}
|
|
|
|
// Try clicking a button to confirm/join. Match common labels case-insensitively.
|
|
const clickTexts = ['set','join','ok','enter','start','confirm'];
|
|
let clicked = false;
|
|
for (const txt of clickTexts) {
|
|
try {
|
|
const xpath = `//button[contains(translate(normalize-space(.),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'${txt}')]`;
|
|
const btns = await page.$x(xpath);
|
|
if (btns && btns.length) {
|
|
await btns[0].click();
|
|
log('Clicked button matching text', txt);
|
|
clicked = true;
|
|
await sleep(500);
|
|
break;
|
|
}
|
|
} catch (inner) {}
|
|
}
|
|
if (!clicked) {
|
|
// fallback: click submit-type inputs/buttons
|
|
const submit = await page.$("input[type=submit], button[type=submit]");
|
|
if (submit) {
|
|
await submit.click();
|
|
log('Clicked submit control');
|
|
clicked = true;
|
|
await sleep(500);
|
|
}
|
|
}
|
|
|
|
if (!filled && !clicked) log('No name input or submit button found to set name');
|
|
} catch (e) {
|
|
log('Could not auto-enter name:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
// Read the MutationObserver probe timestamp (if it recorded when inputs first appeared)
|
|
try {
|
|
const firstSeen = await page.evaluate(() => window.__inputsFirstSeen || null);
|
|
if (firstSeen) {
|
|
const delta = Date.now() - firstSeen;
|
|
log('Probe: inputs first seen at', new Date(firstSeen).toISOString(), `(ago ${delta}ms)`);
|
|
// Save a screenshot named with the first-seen timestamp to make it easy to correlate
|
|
const shotPath = `/workspace/tmp-house-rules-${firstSeen}.png`;
|
|
try {
|
|
await page.screenshot({ path: shotPath, fullPage: true });
|
|
log('Saved input-appearance screenshot to', shotPath);
|
|
} catch (e) {
|
|
log('Failed to save input-appearance screenshot:', e && e.message ? e.message : e);
|
|
}
|
|
// Also save the full HTML at the time inputs first appeared so we can
|
|
// inspect the DOM (some UI frameworks render inputs late).
|
|
try {
|
|
const html = await page.content();
|
|
const outPath = `/workspace/tmp-domcontent-after-inputs-${firstSeen}.html`;
|
|
try {
|
|
fs.writeFileSync(outPath, html);
|
|
log('Saved DOM HTML at input-appearance to', outPath, '(length', html.length + ')');
|
|
} catch (e) {
|
|
log('Failed to write post-input DOM HTML:', e && e.message ? e.message : e);
|
|
}
|
|
// Also capture any intercepted WebSocket sends recorded by the
|
|
// in-page interceptor and dump them to a JSON file for later
|
|
// verification.
|
|
try {
|
|
const sends = await page.evaluate(() => window.__wsSends || []);
|
|
const wsOut = `/workspace/tmp-ws-sends-${firstSeen}.json`;
|
|
try {
|
|
fs.writeFileSync(wsOut, JSON.stringify(sends, null, 2));
|
|
log('Saved intercepted WS sends to', wsOut, '(count', (sends && sends.length) + ')');
|
|
// QUICK ASSERTION: ensure a player-name send with Automaton was captured
|
|
try {
|
|
const parsed = (sends || []).map(s => {
|
|
try { return JSON.parse(s.data); } catch (e) { return null; }
|
|
}).filter(Boolean);
|
|
const hasPlayerName = parsed.some(p => p.type === 'player-name' && ((p.data && p.data.name) === 'Automaton' || (p.name === 'Automaton')));
|
|
if (!hasPlayerName) {
|
|
log('ASSERTION FAILED: No player-name send with name=Automaton found in intercepted WS sends');
|
|
// Write a failure marker file for CI to pick up
|
|
try { fs.writeFileSync('/workspace/tmp-assert-failed-player-name.txt', 'Missing player-name Automaton'); } catch (e) {}
|
|
process.exit(3);
|
|
} else {
|
|
log('Assertion passed: player-name Automaton was sent by client');
|
|
}
|
|
} catch (e) {
|
|
log('Error while asserting WS sends:', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
log('Failed to write intercepted WS sends file:', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
log('Could not read intercepted WS sends from page:', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
log('Could not read page content at input-appearance:', e && e.message ? e.message : e);
|
|
}
|
|
} else {
|
|
log('Probe did not record input first-seen timestamp (window.__inputsFirstSeen is null)');
|
|
}
|
|
} catch (e) {
|
|
log('Could not read window.__inputsFirstSeen:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
// Ensure we still capture intercepted WS sends even if the probe didn't
|
|
// record the input-appearance timestamp. Dump a latest copy and run the
|
|
// same assertion so CI will fail fast when player-name is missing.
|
|
try {
|
|
const sends = await page.evaluate(() => window.__wsSends || []);
|
|
const outLatest = '/workspace/tmp-ws-sends-latest.json';
|
|
try {
|
|
fs.writeFileSync(outLatest, JSON.stringify(sends, null, 2));
|
|
log('Saved intercepted WS sends (latest) to', outLatest, '(count', (sends && sends.length) + ')');
|
|
} catch (e) {
|
|
log('Failed to write latest intercepted WS sends file:', e && e.message ? e.message : e);
|
|
}
|
|
// Run the same assertion against the latest sends if a per-timestamp
|
|
// file was not written earlier.
|
|
try {
|
|
const parsed = (sends || []).map(s => {
|
|
try { return JSON.parse(s.data); } catch (e) { return null; }
|
|
}).filter(Boolean);
|
|
const hasPlayerName = parsed.some(p => p.type === 'player-name' && ((p.data && p.data.name) === 'Automaton' || (p.name === 'Automaton')));
|
|
if (!hasPlayerName) {
|
|
log('ASSERTION FAILED: No player-name send with name=Automaton found in intercepted WS sends (latest)');
|
|
try { fs.writeFileSync('/workspace/tmp-assert-failed-player-name.txt', 'Missing player-name Automaton'); } catch (e) {}
|
|
process.exit(3);
|
|
} else {
|
|
log('Assertion passed (latest): player-name Automaton was sent by client');
|
|
}
|
|
} catch (e) {
|
|
log('Error while asserting WS sends (latest):', e && e.message ? e.message : e);
|
|
}
|
|
} catch (e) {
|
|
log('Could not read intercepted WS sends for latest dump:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
// If the global timeout fired while we were running, abort now.
|
|
if (_timedOut) {
|
|
log('Test aborted due to global timeout flag set after probe check');
|
|
if (browser) try { await browser.close(); } catch (e) {}
|
|
process.exit(124);
|
|
}
|
|
|
|
// Debug: list buttons with their text
|
|
const tButtonScan = Date.now();
|
|
const buttons = await page
|
|
.$$eval("button", (els) =>
|
|
els.map((b) => ({ text: b.innerText, id: b.id || null }))
|
|
)
|
|
.catch(() => []);
|
|
log(
|
|
"Found buttons (first 20) (scan took",
|
|
Date.now() - tButtonScan + "ms):",
|
|
JSON.stringify(buttons.slice(0, 20), null, 2)
|
|
);
|
|
|
|
// Try to open House Rules by clicking the relevant button if present
|
|
const btn = await page.$x("//button[contains(., 'House Rules')]");
|
|
if (btn && btn.length) {
|
|
log("Found House Rules button, clicking");
|
|
await btn[0].click();
|
|
await sleep(500);
|
|
} else {
|
|
log(
|
|
"House Rules button not found by text; attempting fallback selectors"
|
|
);
|
|
// fallback: try a few likely selectors
|
|
const fallbacks = [
|
|
".Actions button",
|
|
"button[aria-label='House Rules']",
|
|
"button[data-testid='house-rules']",
|
|
];
|
|
let clicked = false;
|
|
for (const sel of fallbacks) {
|
|
const tSelStart = Date.now();
|
|
const el = await page.$(sel);
|
|
log(
|
|
"Checked selector",
|
|
sel,
|
|
" (took",
|
|
Date.now() - tSelStart + "ms) ->",
|
|
!!el
|
|
);
|
|
if (el) {
|
|
log("Clicking fallback selector", sel);
|
|
await el.click();
|
|
clicked = true;
|
|
await sleep(500);
|
|
break;
|
|
}
|
|
}
|
|
if (!clicked) console.log("No fallback selector matched");
|
|
}
|
|
|
|
// Wait for the HouseRules dialog to appear
|
|
try {
|
|
const tWaitStart = Date.now();
|
|
await page.waitForSelector(".HouseRules", { timeout: 2000 });
|
|
log(
|
|
"HouseRules dialog is present (waited",
|
|
Date.now() - tWaitStart + "ms)"
|
|
);
|
|
} catch (e) {
|
|
log("HouseRules dialog did not appear");
|
|
// Dump a small HTML snippet around where it might be
|
|
try {
|
|
const snippet = await page.$$eval("body *:nth-child(-n+60)", (els) =>
|
|
els.map((e) => e.outerHTML).join("\n")
|
|
);
|
|
console.log(
|
|
"Body snippet (first ~60 elements):\n",
|
|
snippet.slice(0, 4000)
|
|
);
|
|
} catch (err) {
|
|
log(
|
|
"Could not capture snippet:",
|
|
err && err.message ? err.message : err
|
|
);
|
|
}
|
|
}
|
|
|
|
// Evaluate whether switches are disabled and capture extra debug info
|
|
const switches = await page
|
|
.$$eval(".HouseRules .RuleSwitch", (els) =>
|
|
els.map((e) => ({
|
|
id: e.id || "",
|
|
disabled: e.disabled,
|
|
outer: e.outerHTML,
|
|
}))
|
|
)
|
|
.catch(() => []);
|
|
log("Switches found:", switches.length);
|
|
switches.forEach((s, i) => {
|
|
log(`${i}: id='${s.id}', disabled=${s.disabled}`);
|
|
if (s.outer && s.outer.length > 0) {
|
|
log(` outerHTML (first 200 chars): ${s.outer.slice(0, 200)}`);
|
|
}
|
|
});
|
|
|
|
// Also attempt to log the current game state and name visible in DOM if present
|
|
try {
|
|
const stateText = await page.$eval(".Table, body", (el) =>
|
|
document.body.innerText.slice(0, 2000)
|
|
);
|
|
log(
|
|
"Page text snippet:\n",
|
|
stateText.split("\n").slice(0, 40).join("\n")
|
|
);
|
|
} catch (err) {
|
|
log(
|
|
"Could not extract page text snippet:",
|
|
err && err.message ? err.message : err
|
|
);
|
|
}
|
|
|
|
// Save a screenshot for inspection (workspace is mounted when running container)
|
|
const out = "/workspace/tmp-house-rules.png";
|
|
try {
|
|
const tShot = Date.now();
|
|
await page.screenshot({ path: out, fullPage: true });
|
|
log("Screenshot saved to", out, "(took", Date.now() - tShot + "ms)");
|
|
} catch (e) {
|
|
log("Screenshot failed:", e && e.message ? e.message : e);
|
|
}
|
|
|
|
await browser.close();
|
|
clearGlobalTimeout();
|
|
log("Puppeteer test finished successfully");
|
|
} catch (err) {
|
|
log("Puppeteer test failed:", err && err.stack ? err.stack : err);
|
|
if (browser)
|
|
try {
|
|
await browser.close();
|
|
} catch (e) {}
|
|
process.exit(2);
|
|
}
|
|
})();
|