import assert from "node:assert/strict"; import { readFileSync, createReadStream, statSync } from "node:fs"; import { createServer } from "node:http"; import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; test("MIST gallery scene accepts Skip and menu clicks, keeps video centered and bounds overlapping music", { skip: !process.env.PGAMES_PLAYWRIGHT_PATH || !process.env.PGAMES_MIST_RUNTIME || !process.env.PGAMES_MIST_MANIFEST || !process.env.PGAMES_MIST_BLOBS, }, async () => { const { chromium } = await import(pathToFileURL(process.env.PGAMES_PLAYWRIGHT_PATH + "/index.mjs").href); const runtime = readFileSync(process.env.PGAMES_MIST_RUNTIME); const manifest = { ...JSON.parse(readFileSync(process.env.PGAMES_MIST_MANIFEST)).manifest, galleryUnlocked: true, versionId: 1 }; const compiled = await build({ stdin: { contents: ` import {Engine} from './player/engine'; const r = await (await fetch('/runtime.json')).json(); const manifest = await (await fetch('/manifest.json')).json(); const tr = {lang: 'en', renpyName: null, str: s => s, line: (_, s) => s}; window.audioCalls = []; const e = window.engine = new Engine(document.querySelector('main'), r.game, manifest, r.modules, {list: async () => [], get: async () => null, put: async () => {}, remove: async () => {}}, 'mist-playback-browser', () => {}, tr, async () => tr); e.settings.cps = 0; e.renderer.transitionsOff = true; e.audio.setVolumes({music: 0, sound: 0, voice: 0}); e.audio.setMixer('ambiant', 0); const play = e.audio.play.bind(e.audio); e.audio.play = (...args) => { window.audioCalls.push(args); return play(...args); }; e.openUnlockedGallery();`, resolveDir: fileURLToPath(new URL("../src/", import.meta.url)), loader: "tsx" }, bundle: true, write: false, format: "esm", platform: "browser", logLevel: "silent" }); const css = readFileSync(new URL("../src/player/player.css", import.meta.url)); const server = createServer((req, res) => { if (req.url === "/bundle.js") { res.setHeader("Content-Type", "text/javascript"); res.end(compiled.outputFiles[0].text); return; } if (req.url === "/style.css") { res.setHeader("Content-Type", "text/css"); res.end(css); return; } if (req.url === "/runtime.json") { res.setHeader("Content-Type", "application/json"); res.end(runtime); return; } if (req.url === "/manifest.json") { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(manifest)); return; } const hash = req.url?.match(/^\/api\/versions\/1\/files\/([a-f0-9]{64}\.[a-z0-9]+)$/)?.[1]; if (hash) { const path = resolve(process.env.PGAMES_MIST_BLOBS, hash.slice(0, 2), hash.slice(2, 4), hash); try { const size = statSync(path).size; const range = req.headers.range?.match(/^bytes=(\d+)-(\d*)$/); const start = range ? Number(range[1]) : 0, end = range?.[2] ? Number(range[2]) : size - 1; res.setHeader("Accept-Ranges", "bytes"); res.setHeader("Content-Type", hash.endsWith(".webm") ? "video/webm" : hash.endsWith(".opus") ? "audio/ogg" : hash.endsWith(".webp") ? "image/webp" : "application/octet-stream"); res.setHeader("Content-Length", end - start + 1); if (range) { res.statusCode = 206; res.setHeader("Content-Range", `bytes ${start}-${end}/${size}`); } createReadStream(path, { start, end }).pipe(res); } catch { res.statusCode = 404; res.end(); } return; } res.setHeader("Content-Type", "text/html"); res.end('
'); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); let browser; try { browser = await chromium.launch({ headless: true, channel: process.env.PGAMES_BROWSER_CHANNEL }); const page = await browser.newPage({ viewport: { width: 1000, height: 700 } }); page.setDefaultTimeout(7000); await page.goto(`http://127.0.0.1:${server.address().port}`); await page.waitForFunction(() => window.engine?.screens.shown.has('rewatchgirlselectscreen')); await page.waitForFunction(() => [...document.querySelectorAll('.pg-simgbtn img')].every(el => el.complete && el.naturalWidth > 0)); await page.locator(".pg-simgbtn").nth(1).click(); await page.waitForFunction(() => window.engine.screens.shown.has('robgallery')); await page.evaluate(() => window.engine.screens.shown.get('robgallery').el.dataset.test = 'robgallery'); await page.waitForFunction(() => [...document.querySelectorAll('.pg-simgbtn img')].every(el => el.complete && el.naturalWidth > 0)); await page.locator('[data-test="robgallery"] .pg-simgbtn').nth(1).click(); await page.waitForFunction(() => !!window.engine.onAdvance && !window.engine.screens.shown.size); await page.locator('[data-act="skip"]').click(); await page.getByRole('button', { name: 'End Scene.', exact: true }).waitFor(); await page.getByRole('button', { name: 'Keep Looking.', exact: true }).click(); await page.waitForFunction(() => document.querySelector('.pg-image video, video.pg-image')?.videoWidth > 0); const state = await page.evaluate(() => { const stage = document.querySelector('.pg-stage').getBoundingClientRect(); const video = document.querySelector('.pg-image video, video.pg-image').getBoundingClientRect(); return { dx: video.x - stage.x, dy: video.y - stage.y, width: video.width, height: video.height, stageWidth: stage.width, stageHeight: stage.height, music: [...window.engine.audio.playing.values()].filter(p => p.channel === 'music').length, calls: window.audioCalls.filter(call => call[0] === 'sound').map(call => call[1][0]) }; }); assert.ok(Math.abs(state.dx) < 1 && Math.abs(state.dy) < 1, `video drift: ${JSON.stringify(state)}`); assert.ok(Math.abs(state.width - state.stageWidth) < 1 && Math.abs(state.height - state.stageHeight) < 1); assert.ok(state.music <= 2, `overlapping music tracks: ${state.music}`); assert.equal(new Set(state.calls).size, state.calls.length, "the scene's sounds are not replayed by duplicate interpreter loops"); await page.locator('[data-act="skip"]').click(); await page.getByRole('button', { name: 'End Scene.', exact: true }).click(); await page.waitForFunction(() => window.engine.screens.shown.has('rewatchgirlselectscreen')); const returned = await page.evaluate(() => ({ screens: [...window.engine.screens.shown.keys()], audio: window.engine.audio.playing.size, images: window.engine.renderer.snapshot().length })); assert.deepEqual(returned, { screens: ['rewatchgirlselectscreen'], audio: 0, images: 0 }); } finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); } });