- puppeteer-core ^23.11.1 -> ^25.12.0 (dev only); npm audit: 7 high (basic-ftp via @puppeteer/browsers / proxy-agent, extract-zip) -> 0; production audit stays clean - puppeteer-core 25 is ESM-only and needs Node >= 22.12: the render / sim browser suites load it with await import() instead of createRequire - headless: 'new' -> headless: true (same new headless mode; 'new' is no longer a typed value) - fieldview CPU throttle: page.emulateCPUThrottling() instead of the deprecated page.target() CDP session - THIRD-PARTY-NOTICES version, DESIGN test note
74 lines
4.4 KiB
JavaScript
74 lines
4.4 KiB
JavaScript
// test/sim/loadout.browser.test.js — operator loadouts (DESIGN §16) in the BROWSER sim: headless Chrome loads the
|
|
// served /sim/ modules and /data/*.json through the client's own loader (public/js/battle/runner.js loadBrowserSim),
|
|
// builds a BattleSpec whose units carry skillIndex / moduleId and must resolve the same defs and produce the same result
|
|
// digest as Node (server re-simulation / verification depend on it).
|
|
//
|
|
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/sim/loadout.browser.test.js (or SIM_E2E=1)
|
|
// Run browser test files one at a time. Chrome path: $CHROME_PATH or the macOS default.
|
|
|
|
import { test, describe, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { buildBattleSpec, createBattleFromSpec, resultDigest } from '../../server/sim/spec.js';
|
|
import { DataSource, getDefaultSource } from '../../server/sim/simdata.js';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
const enabled = (process.env.RENDER_E2E === '1' || process.env.SIM_E2E === '1') && existsSync(CHROME);
|
|
const skip = enabled ? false : 'set RENDER_E2E=1 or SIM_E2E=1 (needs Chrome)';
|
|
|
|
describe('operator loadouts in the browser sim', { skip }, () => {
|
|
let srv, browser;
|
|
before(async () => {
|
|
const puppeteer = (await import('puppeteer-core')).default;
|
|
const { startServer } = await import('../../server/index.js');
|
|
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
|
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-first-run'] });
|
|
});
|
|
after(async () => {
|
|
await browser?.close();
|
|
await srv?.close();
|
|
});
|
|
|
|
test('a spec with loadouts resolves the same defs and gives the same digest in Chrome as in Node', async () => {
|
|
const ds = getDefaultSource();
|
|
const tpl = ds.getWave('act1autochess_03');
|
|
const spec = buildBattleSpec({
|
|
battleId: 'lo1', fieldId: 'n:p1', kind: 'normal', seed: 90210, modeId: 'mode_multi_normal', round: 3, stageId: 'act2autochess_m01',
|
|
timeLimit: 110, players: [{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, bonds: {}, playerEffects: [], units: [
|
|
{ uid: 1, chessId: 'chess_char_1_01_a', row: 10, col: 5, dir: 'RIGHT', skillIndex: 0 },
|
|
{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 8, dir: 'RIGHT', skillIndex: 0 },
|
|
{ uid: 3, chessId: 'chess_char_6_11_b', row: 11, col: 4, dir: 'RIGHT', skillIndex: 1, moduleId: 'uniequip_003_mlyss' },
|
|
{ uid: 4, kind: 'token', tokenId: 'token_10030_mlyss_wtrman', ownerUid: 3, row: 12, col: 6 },
|
|
] }],
|
|
spawns: tpl.spawns.filter((s) => !s.action && !s.slot).map((s) => ({ time: s.time, enemyKey: s.key, routeIndex: s.routeIndex, count: s.count, interval: s.interval })),
|
|
routes: tpl.routes, flags: {}, waveId: 'act1autochess_03',
|
|
});
|
|
const nb = createBattleFromSpec(spec, new DataSource(ds.raw, null), { quiet: true, recordEvents: false });
|
|
const nodeUnits = nb.allyUnits.map((u) => [u.uid, u.def.skill?.id ?? null, u.base.maxHp, u.base.atk]);
|
|
const nodeHash = resultDigest(nb.runToEnd(4000)).hash;
|
|
|
|
const page = await browser.newPage();
|
|
const problems = [];
|
|
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
|
page.on('response', (r) => { if (r.status() >= 400 && !/favicon/.test(r.url())) problems.push(`HTTP ${r.status()} ${r.url()}`); });
|
|
await page.goto(`http://127.0.0.1:${srv.port}/sim/spec.js`);
|
|
const out = await page.evaluate(async (s) => {
|
|
const { loadBrowserSim } = await import('/js/battle/runner.js');
|
|
const { spec: S, ds } = await loadBrowserSim();
|
|
const b = S.createBattleFromSpec(s, ds, { quiet: true, recordEvents: false });
|
|
const units = b.allyUnits.map((u) => [u.uid, u.def.skill?.id ?? null, u.base.maxHp, u.base.atk]);
|
|
return { units, hash: S.resultDigest(b.runToEnd(4000)).hash };
|
|
}, spec);
|
|
await page.close();
|
|
assert.deepEqual(problems, []);
|
|
assert.deepEqual(out.units, nodeUnits, 'same loadout defs in the browser');
|
|
assert.equal(nodeUnits[0][1], 'skchr_inside_1');
|
|
assert.equal(nodeUnits[2][1], 'skchr_mlyss_2');
|
|
assert.equal(nodeUnits[3][1], 'sktok_mlyss_wtrman_2');
|
|
assert.equal(out.hash, nodeHash, 'browser and Node results are identical');
|
|
});
|
|
});
|