Add WebGL page cache and runtime checks

This commit is contained in:
2026-06-08 14:39:42 +02:00
parent 119cefd4bd
commit a73dc5725f
11 changed files with 891 additions and 32 deletions
+250
View File
@@ -0,0 +1,250 @@
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);
});