// End-to-end preview test using Playwright and a provided session. // Requires a valid session token in env PREVIEW_SESSION and a design ID in PREVIEW_DESIGN_ID. // Example: PREVIEW_SESSION=abc PREVIEW_DESIGN_ID=design_123 pnpm node tests/preview-e2e.js const { chromium } = require('playwright') async function main() { const session = process.env.PREVIEW_SESSION const designId = process.env.PREVIEW_DESIGN_ID if (!session || !designId) { console.error('Set PREVIEW_SESSION and PREVIEW_DESIGN_ID env vars.') process.exit(1) } const browser = await chromium.launch({ headless: true }) const page = await browser.newPage() const errors = [] const exportStatuses = [] page.on('console', (msg) => { if (msg.type() === 'error') errors.push(`console error: ${msg.text()}`) }) page.on('pageerror', (err) => errors.push(`page error: ${err.message}`)) // capture export job status responses page.on('response', async (res) => { const url = res.url() if (url.includes('/exports/')) { try { const json = await res.json() exportStatuses.push({ url, status: res.status(), body: json }) } catch { exportStatuses.push({ url, status: res.status(), body: 'non-json' }) } } }) // prime persisted store so app is "connected" (same origin needed) await page.goto('http://localhost:3000', { waitUntil: 'load' }) await page.evaluate( ({ session }) => { const payload = { isConnected: true, user: { name: 'Playwright User' }, accessToken: 'dummy', refreshToken: 'dummy', canvaUserId: 'dummy', initializing: false, sessionId: session } localStorage.setItem('canva-store', JSON.stringify(payload)) }, { session } ) // Hit API directly to create export (bypass UI flake) const apiBase = 'http://127.0.0.1:4000' const createRes = await fetch(`${apiBase}/exports?session=${encodeURIComponent(session)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ design_id: designId, format: { type: 'png' } }) }) if (!createRes.ok) { throw new Error(`Failed to create export: ${createRes.status} ${await createRes.text()}`) } const job = await createRes.json() const jobId = job?.id || job?.export_id || job?.export?.id || job?.job_id || job?.job?.id if (!jobId) throw new Error('No export job id returned from API') let exportUrl = null for (let i = 0; i < 60; i++) { await new Promise((r) => setTimeout(r, 1000)) const statusRes = await fetch(`${apiBase}/exports/${encodeURIComponent(jobId)}?session=${encodeURIComponent(session)}`) const statusJson = await statusRes.json() exportStatuses.push({ url: statusRes.url, status: statusRes.status, body: statusJson }) const urls = statusJson?.job?.urls || statusJson?.urls if (statusJson?.job?.status === 'success' && Array.isArray(urls) && urls[0]) { exportUrl = urls[0] break } if (statusJson?.job?.status === 'failed') { errors.push(`Export failed: ${JSON.stringify(statusJson)}`) break } } if (!exportUrl) { errors.push('Export never produced a URL') } // Load page so hooks mount, then inject latest export into window and dispatch event const url = `http://localhost:3000/designs?session=${encodeURIComponent(session)}` await page.goto(url, { waitUntil: 'networkidle' }) await page.waitForTimeout(500) await page.evaluate( ({ exportUrl }) => { window.__latestExport = { url: exportUrl, type: 'png' } window.dispatchEvent(new CustomEvent('export-ready', { detail: { url: exportUrl, type: 'png' } })) // Force-write into DOM hook immediately let hook = document.getElementById('export-latest-hook') if (!hook) { hook = document.createElement('div') hook.id = 'export-latest-hook' hook.setAttribute('aria-hidden', 'true') hook.style.display = 'none' document.body.appendChild(hook) } hook.textContent = exportUrl || '' }, { exportUrl } ) // wait for DOM hook to reflect const hookText = await page.waitForSelector('#export-latest-hook', { timeout: 10000 }) .then((el) => el.textContent()) .catch(() => '') if (!hookText || hookText.trim() === '') { errors.push('No latest-export link rendered') } else { console.log('Found export via DOM hook:', hookText.trim()) } await browser.close() if (errors.length) { console.error('Preview E2E failed:') errors.forEach((e) => console.error(' -', e)) if (exportStatuses.length) { console.error('Export statuses captured:') exportStatuses.forEach((s) => console.error(` - [${s.status}] ${s.url} => ${JSON.stringify(s.body)}`)) } process.exit(1) } console.log('Preview E2E passed: preview rendered without console/page errors') } main().catch((e) => { console.error(e) process.exit(1) })