const { chromium } = require('playwright'); const targetUrl = process.env.WEBGL_RUNTIME_URL || 'http://localhost:3001/'; async function main() { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); const errors = []; page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); }); page.on('pageerror', (error) => errors.push(error.message)); await page.addInitScript(() => { localStorage.removeItem('ai-interactive-fiction-preferences'); }); await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForFunction(() => window.BookTextureRenderer && window.BookLabDebug, null, { timeout: 180000 }); const result = await page.evaluate(async () => { window.BookTextureRenderer.publishSpread(); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); const nav = document.getElementById('webgl_book_navigation'); const slider = document.getElementById('webgl_book_nav_position'); const minLabel = document.getElementById('webgl_book_nav_min_label'); const maxLabel = document.getElementById('webgl_book_nav_max_label'); const textureInfo = window.BookLabDebug.getTextureInfo(); const initialBookState = window.BookLabDebug.getBookState(); const initialSliderMax = slider?.max || null; const initialMinLabel = minLabel?.textContent || ''; const initialMaxLabel = maxLabel?.textContent || ''; const pageSpreadMap = [0, 1, 2, 3, 4, 5].map(page => [page, window.BookLabDebug.mapPageToSpread(page)]); const spreadPageMap = [0, 1, 2, 3].map(spread => [spread, window.BookLabDebug.mapSpreadToPage(spread)]); const pageCache = window.WebGLPageCache || window.moduleRegistry?.getModule?.('webgl-page-cache'); const cacheProbeCanvas = document.createElement('canvas'); cacheProbeCanvas.width = 8; cacheProbeCanvas.height = 8; const cacheProbeContext = cacheProbeCanvas.getContext('2d'); cacheProbeContext.fillStyle = '#000'; cacheProbeContext.fillRect(0, 0, 8, 8); const cacheProbeMeta = { pageIndex: 9999, width: 8, height: 8, cacheKey: 'runtime-probe' }; const cacheStoreResult = await pageCache?.cachePageCanvas?.(cacheProbeMeta, cacheProbeCanvas); const cacheProbeResult = await pageCache?.getPageCanvas?.(cacheProbeMeta); window.BookLabDebug.setPaginationStateForTest({ spreadIndex: 0, spreadCount: 126, writtenPageLimit: 250 }); const grownBookState = window.BookLabDebug.getBookState(); window.BookLabDebug.setPaginationStateForTest({ spreadIndex: 0, spreadCount: 8, writtenPageLimit: 10 }); slider.value = '100'; slider.dispatchEvent(new Event('input', { bubbles: true })); await new Promise(resolve => { const startedAt = Date.now(); const check = () => { if ((window.BookLabDebug?.activeFlips || 0) === 0 || Date.now() - startedAt > 2200) { resolve(); return; } requestAnimationFrame(check); }; requestAnimationFrame(check); }); const clampedSliderValue = slider.value; document.dispatchEvent(new CustomEvent('webgl-book:page-reserve-directive', { detail: { value: 20, unit: 'percent' } })); const percentReserveState = window.BookLabDebug.getBookState(); document.body.classList.add('webgl-mode'); if (!document.getElementById('page_left')) { window.moduleRegistry?.getModule?.('ui-display-handler')?.initializeContainers?.(); } window.moduleRegistry?.getModule?.('webgl-book-scene')?.moveBookToControlOverlay?.(); const pageLeft = document.getElementById('page_left'); let choicesPanel = document.getElementById('choices'); if (!choicesPanel && pageLeft) { choicesPanel = document.createElement('div'); choicesPanel.id = 'choices'; choicesPanel.className = 'container'; pageLeft.appendChild(choicesPanel); } const choicesGroup = document.createElement('div'); choicesGroup.className = 'choices-group'; const choiceButton = document.createElement('button'); choiceButton.className = 'choice-button'; choiceButton.textContent = 'A deliberately long choice label that must stay inside the WebGL overlay without creating horizontal scrolling'; choicesGroup.appendChild(choiceButton); choicesPanel?.appendChild(choicesGroup); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); const gameTitle = document.getElementById('game_title'); const startPrompt = document.getElementById('start_prompt'); const titleDisplay = gameTitle ? window.getComputedStyle(gameTitle).display : 'absent'; const startPromptDisplay = startPrompt ? window.getComputedStyle(startPrompt).display : 'absent'; const pageLeftStyle = pageLeft ? window.getComputedStyle(pageLeft) : null; const choicesStyle = choicesPanel ? window.getComputedStyle(choicesPanel) : null; const buttonStyle = window.getComputedStyle(choiceButton); const overlayLayout = { pageLeftExists: Boolean(pageLeft), choicesPanelExists: Boolean(choicesPanel), pageLeftNoHorizontalScrollbar: pageLeft ? pageLeft.scrollWidth <= pageLeft.clientWidth + 1 : false, choicesNoHorizontalScrollbar: choicesPanel ? choicesPanel.scrollWidth <= choicesPanel.clientWidth + 1 : false, pageLeftOverflowX: pageLeftStyle?.overflowX || null, choicesOverflowX: choicesStyle?.overflowX || null, titleDisplay, startPromptDisplay, buttonColor: buttonStyle.color, buttonBackground: buttonStyle.backgroundColor }; window.BookLabDebug.setPaginationStateForTest({ spreadIndex: 1, spreadCount: 8, writtenPageLimit: 10 }); if (window.BookPagination) { window.BookPagination.spreads = Array.from({ length: 8 }, (_, index) => ({ index, left: [], right: [], pageMeta: {} })); window.BookPagination.currentSpreadIndex = 1; } let targetFlipEventDetail = null; const flipFinished = new Promise(resolve => { document.addEventListener('webgl-book:page-flip-finished', (event) => { targetFlipEventDetail = event.detail || null; resolve(true); }, { once: true }); }); const requestedFlip = await window.BookLabDebug.startPageFlipForTest(1, { force: true, targetSpread: 2 }); const activeFlipsAfterRequest = window.BookLabDebug.activeFlips; let postAdvanceState = null; if (requestedFlip && window.BookLabDebug.activeFlips > 0) { postAdvanceState = window.BookLabDebug.advancePageFlipForTest(); } const activeFlipsAfterAdvance = window.BookLabDebug.activeFlips; const targetFlipFinished = targetFlipEventDetail ? true : await Promise.race([ flipFinished, new Promise(resolve => window.setTimeout(() => resolve(false), 5000)) ]); const postTargetFlipState = window.BookLabDebug.getBookState(); return { navExists: Boolean(nav), initialSliderMax, initialMinLabel, initialMaxLabel, finalSliderMax: slider?.max || null, finalMaxLabel: maxLabel?.textContent || '', initialBookState, pageSpreadMap, spreadPageMap, pageCacheReady: pageCache?.cacheStatus === 'ready', pageCacheProbe: { stored: cacheStoreResult === true, width: cacheProbeResult?.width || 0, height: cacheProbeResult?.height || 0 }, grownBookState, clampedSliderValue, percentReserveState, overlayLayout, requestedFlip, activeFlipsAfterRequest, activeFlipsAfterAdvance, postAdvanceState, targetFlipFinished, targetFlipEventDetail, postTargetFlipState, textureInfo }; }); await browser.close(); const failures = []; const relevantErrors = errors.filter((error) => !/^Failed to load resource: the server responded with a status of 400/.test(error)); if (relevantErrors.length) failures.push(`browser errors: ${relevantErrors.join(' | ')}`); if (!result.navExists) failures.push('bottom navigation missing'); if (result.initialSliderMax !== '300') failures.push(`expected initial slider max 300, got ${result.initialSliderMax}`); if (result.initialMinLabel !== '0') failures.push(`expected min label 0, got ${result.initialMinLabel}`); if (result.initialMaxLabel !== '300') failures.push(`expected initial max label 300, got ${result.initialMaxLabel}`); if (result.initialBookState?.pageCount !== 300) failures.push(`expected initial pageCount 300, got ${result.initialBookState?.pageCount}`); if (result.initialBookState?.pageReserve !== 50) failures.push(`expected initial pageReserve 50, got ${result.initialBookState?.pageReserve}`); if (result.initialBookState?.progress !== 0) failures.push(`expected initial progress 0, got ${result.initialBookState?.progress}`); if (JSON.stringify(result.pageSpreadMap) !== JSON.stringify([[0, 0], [1, 1], [2, 2], [3, 2], [4, 3], [5, 3]])) { failures.push(`unexpected page-to-spread map ${JSON.stringify(result.pageSpreadMap)}`); } if (JSON.stringify(result.spreadPageMap) !== JSON.stringify([[0, 0], [1, 1], [2, 2], [3, 4]])) { failures.push(`unexpected spread-to-page map ${JSON.stringify(result.spreadPageMap)}`); } if (!result.pageCacheReady) failures.push('WebGL page cache is not ready'); if (!result.pageCacheProbe?.stored || result.pageCacheProbe?.width !== 8 || result.pageCacheProbe?.height !== 8) { failures.push(`WebGL page cache probe failed: ${JSON.stringify(result.pageCacheProbe)}`); } if (result.grownBookState?.pageCount !== 310) failures.push(`expected page count to grow to 310 at writable limit, got ${result.grownBookState?.pageCount}`); if (result.finalSliderMax !== '310') failures.push(`expected final slider max 310, got ${result.finalSliderMax}`); if (result.finalMaxLabel !== '310') failures.push(`expected final max label 310, got ${result.finalMaxLabel}`); if (result.clampedSliderValue !== '10') failures.push(`expected slider clamp to written page 10, got ${result.clampedSliderValue}`); if (result.percentReserveState?.pageReserve !== 62) failures.push(`expected 20% reserve of 310 pages to be 62, got ${result.percentReserveState?.pageReserve}`); if (!result.overlayLayout?.pageLeftNoHorizontalScrollbar) failures.push('WebGL overlay page_left has a horizontal scrollbar'); if (!result.overlayLayout?.choicesNoHorizontalScrollbar) failures.push('WebGL choices panel has a horizontal scrollbar'); if (result.overlayLayout?.pageLeftOverflowX !== 'hidden') failures.push(`expected page_left overflow-x hidden, got ${result.overlayLayout?.pageLeftOverflowX}`); if (result.overlayLayout?.choicesOverflowX !== 'hidden') failures.push(`expected choices overflow-x hidden, got ${result.overlayLayout?.choicesOverflowX}`); if (!['none', 'absent'].includes(result.overlayLayout?.titleDisplay)) failures.push(`expected title hidden in WebGL overlay, got ${result.overlayLayout?.titleDisplay}`); if (!['none', 'absent'].includes(result.overlayLayout?.startPromptDisplay)) failures.push(`expected start prompt hidden in WebGL overlay, got ${result.overlayLayout?.startPromptDisplay}`); if (/^rgb\(0,\s*0,\s*0\)$/.test(result.overlayLayout?.buttonColor || '')) failures.push('choice button text is still black in WebGL overlay'); if (!result.requestedFlip) failures.push('targeted page flip request was rejected'); if (!result.targetFlipFinished) failures.push(`targeted page flip did not finish: ${JSON.stringify({ requestedFlip: result.requestedFlip, activeFlipsAfterRequest: result.activeFlipsAfterRequest, activeFlipsAfterAdvance: result.activeFlipsAfterAdvance, postAdvanceState: result.postAdvanceState, eventDetail: result.targetFlipEventDetail })}`); if (result.postTargetFlipState?.spreadIndex !== 2) failures.push(`targeted page flip should commit spread 2, got ${result.postTargetFlipState?.spreadIndex}`); if (!result.textureInfo?.debug?.left?.painted || !result.textureInfo?.debug?.right?.painted) failures.push('page texture publish did not paint both pages'); if (failures.length) { console.error('WebGL runtime regression checks failed:'); failures.forEach(failure => console.error(`- ${failure}`)); process.exit(1); } console.log('WebGL runtime regression checks passed.'); } main().catch((error) => { console.error(error); process.exit(1); });