0.2.6-dev: the browser suites load the served manifests' content-hashed URLs (e2e servers wait for the asset index) and check Spine, audio and art through them; the history note of host bandwidth layers 0 and 1 for the next version; golden: unchanged
This commit is contained in:
1 parent
0bb16fd616
commit
409fec90d5
5 files changed
+155
-3
No files matched your search
@@ -109,6 +109,13 @@ export async function startRealServer(opts = {}) {
|
||||
if (Date.now() - t0 > 20000) { child.kill('SIGKILL'); throw new Error(`server did not start:\n${logs.join('\n')}`); }
|
||||
await sleep(150);
|
||||
}
|
||||
// the content-hash index (server/http/assetIndex.js, built ~1 s after the start): wait for it, so a page loads the
|
||||
// served manifest's /assets/~<hash>/… URLs like a player's would (none: SP_ASSET_INDEX=0); bounded, never fatal
|
||||
for (const t1 = Date.now(); Date.now() - t1 < 20000;) {
|
||||
const h = await fetch(`${base}/healthz?traffic=1`).then((r) => r.json(), () => null);
|
||||
if (!h || !h.assetHashes || h.assetHashes.ready) break;
|
||||
await sleep(150);
|
||||
}
|
||||
return {
|
||||
base, port, logs,
|
||||
// `{ hard: true }`: SIGKILL at once — a crash: no room.closed 'shutdown' reaches the clients
|
||||
|
||||
@@ -23,6 +23,8 @@ describe('render engine in headless Chrome', { skip }, () => {
|
||||
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 });
|
||||
// the content-hash index first: every page then loads the served manifest's /assets/~<hash>/… URLs (ASSETS.md)
|
||||
await srv.assetIndex?.whenReady();
|
||||
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-first-run'] });
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
});
|
||||
@@ -73,6 +75,39 @@ describe('render engine in headless Chrome', { skip }, () => {
|
||||
});
|
||||
}
|
||||
|
||||
test('Spine models load through the served manifest\'s content-hashed URLs, cached immutably (host bandwidth layer 1)', async (t) => {
|
||||
if (!srv.assetIndex) { t.skip('SP_ASSET_INDEX=0: no content hashes'); return; }
|
||||
const page = await browser.newPage();
|
||||
const spine = [];
|
||||
const problems = [];
|
||||
page.on('response', (r) => {
|
||||
const u = new URL(r.url());
|
||||
if (/\.(skel|atlas)$/.test(u.pathname) || (/\/spine\//.test(u.pathname) && /\.png$/.test(u.pathname))) spine.push({ path: u.pathname, status: r.status(), cache: r.headers()['cache-control'] });
|
||||
if (r.status() >= 400) problems.push(`HTTP ${r.status()} ${r.url()}`);
|
||||
});
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
try {
|
||||
await page.setViewport({ width: 1600, height: 900 });
|
||||
await page.goto(`http://127.0.0.1:${srv.port}/dev/render-demo.html?scene=normal-m01&t=18&panel=0`);
|
||||
await page.waitForFunction('window.__demo && (window.__demo.ready || window.__demo.error)', { timeout: 30000 });
|
||||
await wait(3500);
|
||||
const st = await page.evaluate(() => window.__demo.stats());
|
||||
assert.ok(st.units > 0);
|
||||
assert.equal(st.spine.failed, 0, 'no Spine load failures');
|
||||
assert.ok(spine.length >= 6, `Spine files requested: ${spine.length}`);
|
||||
for (const s of spine) {
|
||||
assert.match(s.path, /^\/assets\/~[0-9a-f]{16}\//, `${s.path}: a content-hashed URL`);
|
||||
assert.equal(s.status, 200, s.path);
|
||||
assert.equal(s.cache, 'public, max-age=31536000, immutable', `${s.path}: skeleton, atlas and pages share the model hash`);
|
||||
}
|
||||
// the atlas pixi-spine derived from each skeleton came under the skeleton's segment
|
||||
for (const sk of spine.filter((s) => s.path.endsWith('.skel'))) {
|
||||
assert.ok(spine.some((s) => s.path === sk.path.replace(/\.skel$/, '.atlas')), `${sk.path}: its atlas`);
|
||||
}
|
||||
assert.deepEqual(problems, []);
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
|
||||
test('Swire coins from a real snapshot remain separate from the elemental gauge', async () => {
|
||||
const { makeBattle } = await import('../helpers/battleHarness.js');
|
||||
const h = makeBattle({ autoFinish: false, units: [{ uid: 1, chessId: 'chess_char_3_04_a', row: 10, col: 4, skillIndex: 1 }] });
|
||||
|
||||
@@ -11,7 +11,10 @@ test('buying Matterhorn sends two live layer cues through the real client; resyn
|
||||
const srv = await startRealServer({ fast: { timerScale: 0.1, startRound: 2, kit: 0, shop: ['chess_char_1_02_a'] } });
|
||||
const c = new Client(P, srv.base, 'bond-audio', { prefix: 'bond-audio' });
|
||||
try {
|
||||
await c.open(); await c.enter('叠层音效测试');
|
||||
await c.open();
|
||||
const mediaHits = [];
|
||||
c.page.on('response', (r) => { const p = new URL(r.url()).pathname; if (p.startsWith('/media/')) mediaHits.push({ p, status: r.status(), cache: r.headers()['cache-control'] }); });
|
||||
await c.enter('叠层音效测试');
|
||||
await c.click('.mode-card', '独立模拟'); await c.click('.diff-card', '险境');
|
||||
await c.click('.create-box button', '开始独立模拟');
|
||||
await c.waitFor(s => !!s.room, 'solo room');
|
||||
@@ -41,6 +44,15 @@ test('buying Matterhorn sends two live layer cues through the real client; resyn
|
||||
assert.deepEqual(result.frames.map(m => [m.bondId, m.add]).sort(), [['kjeragShip', 2], ['steadShip', 2]]);
|
||||
assert.equal(result.cues.length, 2); assert.equal(result.playing, true); assert.equal(result.state, 'running');
|
||||
assert.ok(result.cues.every(x => /act1autochess_b_ui_buffup/.test(x.url)));
|
||||
// host bandwidth layer 1: the served manifest names the cue with its content hash and the client fetches it through
|
||||
// the extension-less /media/~<hash>/… route, cached immutably (skipped with SP_ASSET_INDEX=0: plain URLs)
|
||||
if (result.cues[0].url.includes('/~')) {
|
||||
assert.ok(result.cues.every(x => /^\/assets\/~[0-9a-f]{16}\/audio\//.test(x.url)), JSON.stringify(result.cues));
|
||||
const hit = mediaHits.find(h => /^\/media\/~[0-9a-f]{16}\/.*act1autochess_b_ui_buffup$/.test(h.p));
|
||||
assert.ok(hit, mediaHits.map(h => h.p).join(' '));
|
||||
assert.equal(hit.status, 200);
|
||||
assert.equal(hit.cache, 'public, max-age=31536000, immutable');
|
||||
}
|
||||
const count = result.cues.length;
|
||||
// g.watch on own prep asks the real server for state again, without a new gain.
|
||||
await c.page.evaluate(() => globalThis.__SP__.net.request('g.watch', { fieldId: `n:${globalThis.__SP__.store.get().me.playerId}` }));
|
||||
|
||||
@@ -38,7 +38,7 @@ const CHROME_ARGS = ['--no-sandbox', '--no-first-run', '--autoplay-policy=no-use
|
||||
*/
|
||||
class Client {
|
||||
constructor(puppeteer, base, label, { w = 1920, h = 1080 } = {}) {
|
||||
Object.assign(this, { puppeteer, base, label, w, h, problems: [], log: [], shots: 0 });
|
||||
Object.assign(this, { puppeteer, base, label, w, h, problems: [], log: [], shots: 0, hashed: { spine: 0, media: 0, other: 0 } });
|
||||
}
|
||||
|
||||
async open(query = '') {
|
||||
@@ -50,7 +50,14 @@ class Client {
|
||||
page.on('console', (m) => { if (m.type() === 'error') this.problems.push(`console: ${m.text()}`); });
|
||||
page.on('pageerror', (e) => this.problems.push(`pageerror: ${e.message}`));
|
||||
page.on('requestfailed', (r) => this.problems.push(`requestfailed: ${r.url()} ${r.failure()?.errorText}`));
|
||||
page.on('response', (r) => { if (r.status() >= 400) this.problems.push(`http ${r.status()}: ${r.url()}`); });
|
||||
page.on('response', (r) => {
|
||||
if (r.status() >= 400) this.problems.push(`http ${r.status()}: ${r.url()}`);
|
||||
// the served manifest's content-hashed URLs (docs/ASSETS.md): Spine files, audio through /media/~<hash>/, the rest
|
||||
const p = new URL(r.url()).pathname;
|
||||
if (r.status() === 200 && /^\/(?:assets|media)\/~[0-9a-f]{16}\//.test(p)) {
|
||||
this.hashed[/\.(?:skel|atlas)$|\/spine\//.test(p) ? 'spine' : p.startsWith('/media/') ? 'media' : 'other']++;
|
||||
}
|
||||
});
|
||||
page.on('dialog', (d) => d.dismiss().catch(() => {}));
|
||||
await page.goto(`${this.base}/${query}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!globalThis.__SP__ && !!document.querySelector('.screen'), { timeout: 30000 });
|
||||
@@ -443,6 +450,8 @@ describe('real server + real browsers', { skip: !ENABLED && 'set SP_REAL_E2E=1 (
|
||||
const { startServer } = await import('../../server/index.js');
|
||||
puppeteer = (await import('puppeteer-core')).default;
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
// the content-hash index first, so the pages load the served manifests' /assets/~<hash>/… URLs like a player's
|
||||
await srv.assetIndex?.whenReady();
|
||||
base = `http://127.0.0.1:${srv.port}`;
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
});
|
||||
@@ -628,6 +637,10 @@ describe('real server + real browsers', { skip: !ENABLED && 'set SP_REAL_E2E=1 (
|
||||
assert.ok(did.inspected && did.bond, 'detail panel + bond popup');
|
||||
assert.ok(did.emote, 'emote bubble reached the teammate');
|
||||
assert.ok(did.enemies >= 0, 'enemy drawer opened');
|
||||
if (srv.assetIndex) {
|
||||
host.note(`content-hashed responses: ${JSON.stringify(host.hashed)}`);
|
||||
assert.ok(host.hashed.spine > 0 && host.hashed.media > 0 && host.hashed.other > 0, `Spine, audio and art through hashed URLs: ${JSON.stringify(host.hashed)}`);
|
||||
}
|
||||
await assertClean(both);
|
||||
} finally {
|
||||
console.log([...host.log, ...guest.log].join('\n'));
|
||||
|
||||
Reference in new issue
Block a user