283 lines
16 KiB
JavaScript
283 lines
16 KiB
JavaScript
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 runtimeInvariants = window.BookLabDebug.getRuntimeInvariants?.() || {};
|
|
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
|
|
});
|
|
const initialNavigationDisabled = {
|
|
topBackward: Boolean(document.getElementById('flip_backward')?.disabled),
|
|
topFastBackward: Boolean(document.getElementById('fast_flip_backward')?.disabled),
|
|
bottomStart: Boolean(document.getElementById('webgl_book_nav_start')?.disabled),
|
|
bottomBack: Boolean(document.getElementById('webgl_book_nav_back')?.disabled)
|
|
};
|
|
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();
|
|
window.BookLabDebug.setPaginationStateForTest({
|
|
spreadIndex: 5,
|
|
spreadCount: 8,
|
|
writtenPageLimit: 10
|
|
});
|
|
const endNavigationDisabled = {
|
|
topForward: Boolean(document.getElementById('flip_forward')?.disabled),
|
|
topFastForward: Boolean(document.getElementById('fast_flip_forward')?.disabled),
|
|
bottomForward: Boolean(document.getElementById('webgl_book_nav_forward')?.disabled),
|
|
bottomEnd: Boolean(document.getElementById('webgl_book_nav_end')?.disabled)
|
|
};
|
|
|
|
return {
|
|
navExists: Boolean(nav),
|
|
runtimeInvariants,
|
|
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,
|
|
initialNavigationDisabled,
|
|
clampedSliderValue,
|
|
percentReserveState,
|
|
overlayLayout,
|
|
requestedFlip,
|
|
activeFlipsAfterRequest,
|
|
activeFlipsAfterAdvance,
|
|
postAdvanceState,
|
|
targetFlipFinished,
|
|
targetFlipEventDetail,
|
|
postTargetFlipState,
|
|
endNavigationDisabled,
|
|
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 (Math.abs(Number(result.runtimeInvariants?.targetFrameDurationMs || 0) - (1000 / 60)) > 0.001) {
|
|
failures.push(`expected 60fps target frame duration, got ${result.runtimeInvariants?.targetFrameDurationMs}`);
|
|
}
|
|
if (result.runtimeInvariants?.flipFrontBackShareMaterial) failures.push('flip front/back materials are shared instead of independently switchable');
|
|
if (!result.runtimeInvariants?.mirrorRefreshesEveryFrame) failures.push('mirror reflection is not marked for per-frame refresh');
|
|
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.initialNavigationDisabled?.topBackward || !result.initialNavigationDisabled?.topFastBackward || !result.initialNavigationDisabled?.bottomStart || !result.initialNavigationDisabled?.bottomBack) {
|
|
failures.push(`backward navigation should be disabled at first page: ${JSON.stringify(result.initialNavigationDisabled)}`);
|
|
}
|
|
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.endNavigationDisabled?.topForward || !result.endNavigationDisabled?.topFastForward || !result.endNavigationDisabled?.bottomForward || !result.endNavigationDisabled?.bottomEnd) {
|
|
failures.push(`forward navigation should be disabled at written end: ${JSON.stringify(result.endNavigationDisabled)}`);
|
|
}
|
|
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);
|
|
});
|