import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { spawn } from 'node:child_process'; import { delay, evaluate, launchChromium } from './raw-cdp-browser.mjs'; const host = '127.0.0.1'; const appRoot = resolve(import.meta.dirname, '..'); const viteEntrypoint = resolve(appRoot, 'node_modules/vite/bin/vite.js'); const browserProfile = await mkdtemp(join(tmpdir(), 'cartesium-explicit-sampling-')); let vite; let chromium; let browser; try { const port = await findFreePort(); const origin = `http://${host}:${port}`; vite = spawn( process.execPath, [viteEntrypoint, 'dev', '--host', host, '--port', String(port), '--strictPort'], { cwd: appRoot, stdio: ['ignore', 'pipe', 'pipe'] } ); const viteOutput = []; collectOutput(vite, viteOutput); await waitForServer(`${origin}/calc/2d`, vite, viteOutput); const launched = await launchChromium(browserProfile); chromium = launched.process; browser = launched.browser; const { targetId } = await browser.command('Target.createTarget', { url: 'about:blank' }); const { sessionId } = await browser.command('Target.attachToTarget', { targetId, flatten: true }); const command = (method, params = {}) => browser.command(method, params, sessionId); await command('Page.enable'); await command('Runtime.enable'); await command('Emulation.setDeviceMetricsOverride', { width: 1300, height: 660, deviceScaleFactor: 1, mobile: false }); await command('Page.navigate', { url: `${origin}/calc/2d` }); await waitForEvaluation(command, `!!document.querySelector('button[aria-label="Expression text view"]')`, 'calculator'); await evaluate(command, `document.querySelector('button[aria-label="Expression text view"]').click()`); await waitForEvaluation(command, `!!document.querySelector('textarea[aria-label="All graph expressions"]')`, 'batch editor'); await evaluate(command, `(() => { const editor = document.querySelector('textarea[aria-label="All graph expressions"]'); editor.value = 'sin(1/ln(x))'; editor.dispatchEvent(new Event('input', { bubbles: true })); })()`); await evaluate(command, `document.querySelector('button[aria-label="Expression UI view"]').click()`); await evaluate(command, `(() => { const settings = [...document.querySelectorAll('details')].find(d => d.querySelector('summary')?.textContent.trim() === 'Graph settings'); settings.open = true; window.__explicitGraphSettings = settings; const checkbox = settings.querySelector('.equal-scale input'); if (checkbox.checked) checkbox.click(); })()`); await delay(200); const views = [ [-0.576, 3.083, -1.21, 1.36], [-0.30, 2.52, -0.93, 1.04], [0.064, 2.036, -0.335, 1.032], [0.25, 1.79, -0.065, 1.005], [0.475, 1.539, 0.29, 1.023], [0.625, 1.458, 0.43, 1.002], [0.695, 1.337, 0.55, 1.003], [0.735, 1.305, 0.604, 1.002] ]; const results = []; for (const bounds of views) { await evaluate(command, `(() => { const inputs = [...window.__explicitGraphSettings.querySelectorAll('.bounds-grid input')]; const bounds = ${JSON.stringify(bounds)}; inputs.forEach((input, index) => { input.value = String(bounds[index]); input.dispatchEvent(new Event('input', { bubbles: true })); }); })()`); await delay(400); const state = await evaluate(command, `(() => { const canvas = document.querySelector('.plot-canvas'); const bounds = ${JSON.stringify(bounds)}; const actualBounds = [...window.__explicitGraphSettings.querySelectorAll('.bounds-grid input')].map(i => Number(i.value)); const ctx = canvas.getContext('2d'); const peaks = [Math.exp(-2/(3*Math.PI)), Math.exp(2/(5*Math.PI))]; const pixels = peaks.map(x => { const px = Math.round((x - bounds[0])/(bounds[1] - bounds[0])*canvas.width); const py = Math.round((bounds[3] - 1)/(bounds[3] - bounds[2])*canvas.height); const left = Math.max(0, px - 6), top = Math.max(0, py - 6); const data = ctx.getImageData(left, top, Math.min(13, canvas.width-left), Math.min(13, canvas.height-top)).data; let count = 0; for (let i=3; i count > 0), `both mathematically known peaks must be painted: ${JSON.stringify({ bounds, ...state })}`); results.push({ bounds, ...state }); } if (process.env.EXPLICIT_SAMPLING_SCREENSHOT) { const screenshot = await command('Page.captureScreenshot', { format: 'png' }); await writeFile(process.env.EXPLICIT_SAMPLING_SCREENSHOT, Buffer.from(screenshot.data, 'base64')); } console.log(JSON.stringify({ views: results })); } finally { browser?.socket.close(); chromium?.kill(); vite?.kill(); await rm(browserProfile, { recursive: true, force: true }); } async function waitForEvaluation(command, expression, label, timeoutMs = 30_000, ready = Boolean) { const deadline = Date.now() + timeoutMs; let last; while (Date.now() < deadline) { last = await evaluate(command, expression); if (ready(last)) return last; await delay(100); } throw new Error(`Timed out waiting for ${label}; last browser value: ${JSON.stringify(last)}`); } async function findFreePort() { const server = createServer(); await new Promise((resolveListen, reject) => { server.once('error', reject); server.listen(0, host, resolveListen); }); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Could not find an available port.'); const { port } = address; await new Promise((resolveClose, reject) => server.close((error) => (error ? reject(error) : resolveClose())) ); return port; } async function waitForServer(url, child, output) { const deadline = Date.now() + 30_000; while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`Vite exited: ${output.slice(-10).join('')}`); try { if ((await fetch(url)).ok) return; } catch { // Vite is still starting. } await delay(100); } throw new Error(`Timed out waiting for ${url}: ${output.slice(-10).join('')}`); } function collectOutput(child, output) { for (const stream of [child.stdout, child.stderr]) { stream?.on('data', (chunk) => output.push(chunk.toString())); } }