Files
pgames-manager/frontend/tests/mist-gallery-playback-browser.test.mjs
T
mrleo1nid 4dc660f786
CI / frontend (push) Failing after 20s
CI / backend (push) Failing after 36s
Fix MIST gallery scene controls, video positioning and audio overlap
2026-10-27 16:14:13 +03:00

98 lines
6.9 KiB
JavaScript

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('<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="/style.css"><style>body{margin:0}main{width:960px;height:540px}</style><main></main><script type="module" src="/bundle.js"></script>');
});
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));
}
});