feedback5: merge W5Z — the co-op briefing's top bar fits the third tab (自选编队) in zh and en at every width (the only product bug of the 0.2.0 full pass); three tests hardened against load (client-wait counts calls, leftovers waits for the item to fly home, bossfield checks 瘫痪 on copies with a golden 催泪瓦斯); golden unchanged
This commit is contained in:
commit
b9b647eecf
5 files changed
+116
-31
No files matched your search
@@ -27,12 +27,34 @@ function fakePage(trueAfter) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A page whose predicate holds from its `holdsOn`-th waitForFunction call on; each earlier slice times out after its own
|
||||
* timeout, like puppeteer's. Counted in calls, not in wall time: on a loaded machine late timers merge time-based slices
|
||||
* (fakePage(95) with 30 ms slices returned in 3 calls instead of 4 in the 0.2.0 candidate's full pass).
|
||||
*/
|
||||
function slicedPage(holdsOn) {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
waitForFunction(fn, opts, ...args) {
|
||||
calls.push({ fn, opts, args });
|
||||
const holds = calls.length >= holdsOn;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (holds) setTimeout(() => resolve({ handle: 'ok', args }), 0);
|
||||
else setTimeout(() => reject(timeoutError(opts.timeout)), opts.timeout);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('a wait longer than one slice keeps polling the same predicate until it holds', async () => {
|
||||
const page = fakePage(95);
|
||||
const page = slicedPage(4);
|
||||
const fn = () => true;
|
||||
const got = await waitForFunctionLong(page, fn, { timeout: 1000, polling: 200, slice: 30 }, 'a', 2);
|
||||
const t0 = Date.now();
|
||||
const got = await waitForFunctionLong(page, fn, { timeout: 60_000, polling: 200, slice: 30 }, 'a', 2);
|
||||
assert.deepEqual(got, { handle: 'ok', args: ['a', 2] });
|
||||
assert.ok(page.calls.length >= 4, `sliced (${page.calls.length} calls)`);
|
||||
assert.equal(page.calls.length, 4, 'three slices ran out, the fourth held, nothing polled after it');
|
||||
assert.ok(Date.now() - t0 >= 85, `the three slices waited their 30 ms each (${Date.now() - t0} ms)`);
|
||||
for (const c of page.calls) {
|
||||
assert.equal(c.fn, fn);
|
||||
assert.deepEqual(c.args, ['a', 2]);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
// with the field's LocalBossPool, and must give the same result digest, the same pool and the same fx per kind.
|
||||
// The fields exercise the playtest-6b sim paths: 直接乘算 bonus sums (§20.10), one 奥术 instance per target
|
||||
// (applyStrongest), the drone 2 % links and the 剑 / 锤 / shell kit (§20.13), and 限伤 cancels (fx 'hitCap', §20.12).
|
||||
// 瘫痪 (fx 'palsy': a 麻痹 stack interrupts an enemy attack) is checked on the same four specs with a golden 催泪瓦斯 in
|
||||
// every operator's second slot (withTearGas), compared Chrome vs Node like the others: the bots' only 麻痹 had been one
|
||||
// 5 % 催泪瓦斯 on one operator — a single hit at 0.1.4, none since 0.2.0's enemy damage frame (WE1) re-timed every fight.
|
||||
//
|
||||
// Opt-in (starts Chrome): SIM_E2E=1 node --test test/sim/bossfield.browser.test.js (or RENDER_E2E=1)
|
||||
// Run browser test files one at a time. Chrome path: $CHROME_PATH or the macOS default.
|
||||
@@ -59,6 +62,21 @@ function captureBossSpecs() {
|
||||
return specs;
|
||||
}
|
||||
|
||||
/**
|
||||
* A copy of `spec` where every operator carries a golden 催泪瓦斯 (攻击时有5%概率使目标获得一层麻痹) in its second slot —
|
||||
* two slots, as in the game: a 麻痹 source on every field whatever the bots drafted (seed 22's own was a single 5 % roll).
|
||||
*/
|
||||
const TEAR_GAS = 'chess_item_5_01_e_b';
|
||||
function withTearGas(spec) {
|
||||
const s = JSON.parse(JSON.stringify(spec));
|
||||
for (const p of s.players || []) {
|
||||
for (const u of p.units || []) {
|
||||
if (u.kind === 'chess') u.items = [...(u.items || []).filter((id) => !String(id).startsWith('chess_item_5_01_e')).slice(0, 1), TEAR_GAS];
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a spec to the end (the same code in Node and in the page: `S` = spec module, `ds` = data source) and returns
|
||||
* its digest, the local pool, the fx count per kind and the leader-side checks (奥术 instances, 无来源 HP losses).
|
||||
@@ -100,8 +118,10 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () =
|
||||
test('real boss specs at 999 layers: Chrome and Node give the same digest, pool and fx; 限伤, drones, 瘫痪 and 奥术 all occur', { timeout: 180000 }, async () => {
|
||||
const specs = captureBossSpecs();
|
||||
assert.deepEqual(specs.map((s) => `${s.kind}:${s.bossId}`).sort(), ['boss:boss_1', 'boss:boss_1', 'hidden:boss_8', 'hidden:boss_8']);
|
||||
// the four as captured, then the same four with a 麻痹 source on every field (withTearGas)
|
||||
const runs = [...specs, ...specs.map(withTearGas)];
|
||||
const ds = new DataSource(DATA, null);
|
||||
const node = specs.map((spec) => runField({ createBattleFromSpec, resultDigest }, ds, spec, MAX_SECONDS));
|
||||
const node = runs.map((spec) => runField({ createBattleFromSpec, resultDigest }, ds, spec, MAX_SECONDS));
|
||||
|
||||
const page = await browser.newPage();
|
||||
const problems = [];
|
||||
@@ -115,12 +135,12 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () =
|
||||
const { spec: S, ds } = await loadBrowserSim();
|
||||
const run = new Function(`return (${src})`)();
|
||||
return all.map((spec) => run(S, ds, spec, maxSeconds));
|
||||
}, specs, runField.toString(), MAX_SECONDS);
|
||||
}, runs, runField.toString(), MAX_SECONDS);
|
||||
await page.close();
|
||||
assert.deepEqual(problems, []);
|
||||
|
||||
specs.forEach((spec, i) => {
|
||||
const at = `${spec.kind} ${spec.fieldId} ${spec.bossId}`;
|
||||
runs.forEach((spec, i) => {
|
||||
const at = `${spec.kind} ${spec.fieldId} ${spec.bossId}${i >= specs.length ? ' +催泪瓦斯' : ''}`;
|
||||
assert.equal(node[i].reason, 'cleared', `${at}: the 999-layer pair clears the field`);
|
||||
assert.equal(chrome[i].json, node[i].json, `${at}: browser and Node results are identical`);
|
||||
assert.equal(chrome[i].hash, node[i].hash, at);
|
||||
@@ -129,10 +149,12 @@ describe('Final Assault / Hidden Core fields in the browser sim', { skip }, () =
|
||||
assert.equal(chrome[i].hpLoss, node[i].hpLoss, `${at}: same 无来源 HP losses on the leader`);
|
||||
assert.ok(node[i].maxArcane <= 1, `${at}: one 奥术 instance on the leader at a time (${node[i].maxArcane})`);
|
||||
});
|
||||
const sum = (k) => node.reduce((a, r) => a + (r.fx[k] || 0), 0);
|
||||
assert.ok(sum('hitCap') >= 1, `a 999-layer hit reached 300000 and was cancelled (限伤: ${sum('hitCap')})`);
|
||||
assert.ok(sum('palsy') >= 1, `瘫痪 occurred (${sum('palsy')})`);
|
||||
assert.ok(node.every((r) => r.hpLoss >= 1), `every field cost the leader 无来源 HP (drone links / 剑 · 锤 transfers: ${node.map((r) => r.hpLoss)})`);
|
||||
assert.ok(node.some((r) => r.maxArcane === 1), 'the leader carried 奥术');
|
||||
const real = node.slice(0, specs.length);
|
||||
const gassed = node.slice(specs.length);
|
||||
const sum = (rows, k) => rows.reduce((a, r) => a + (r.fx[k] || 0), 0);
|
||||
assert.ok(sum(real, 'hitCap') >= 1, `a 999-layer hit reached 300000 and was cancelled (限伤: ${sum(real, 'hitCap')})`);
|
||||
assert.ok(sum(gassed, 'palsy') >= 1, `瘫痪 occurred with the 催泪瓦斯 (${gassed.map((r) => r.fx.palsy || 0)})`);
|
||||
assert.ok(real.every((r) => r.hpLoss >= 1), `every field cost the leader 无来源 HP (drone links / 剑 · 锤 transfers: ${real.map((r) => r.hpLoss)})`);
|
||||
assert.ok(real.some((r) => r.maxArcane === 1), 'the leader carried 奥术');
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,8 @@
|
||||
// 4. solo battle: the pause button sends g.pause {on}, m.public.paused shows the overlay and freezes the countdown,
|
||||
// Space resumes; no pause control in co-op or in prep.
|
||||
// Real server (test/e2e/fastServer.mjs, real clicks and canvas drags):
|
||||
// 1+4. a solo run: equip two items on an operator, drop a third → dialog → 取消 (nothing sent) → pick the OLDER item →
|
||||
// 1+4. a solo run: equip two items on an operator, drop a third → dialog → 取消 (nothing sent, the item drawn back on its
|
||||
// bench slot) → pick the OLDER item →
|
||||
// the real engine destroys exactly that one; then ready → the real local battle → pause (the battle clock and the
|
||||
// countdown stand still) → 继续作战;
|
||||
// 3. the server restarts while the briefing is on screen → 「服务器会话已重置,上一局模拟已结束」 and the lobby.
|
||||
@@ -275,6 +276,7 @@ describe('client leftovers — real server', { skip: !ENABLED && 'set SP_E2E=1 (
|
||||
const equipped = await itemsOf();
|
||||
assert.deepEqual(equipped.map((x) => x.id), KIT_ITEMS.slice(0, 2));
|
||||
const third = await handItem(KIT_ITEMS[2]);
|
||||
const benchSlot = await c.piecePoint(third.uid, 0.5); // where the view draws it in the hand
|
||||
|
||||
// the third: dialog → 取消 → nothing sent / changed
|
||||
await dropItem(third);
|
||||
@@ -289,6 +291,15 @@ describe('client leftovers — real server', { skip: !ENABLED && 'set SP_E2E=1 (
|
||||
assert.equal((await c.requests('g.equip')).length, sentBefore, '取消: no g.equip');
|
||||
assert.deepEqual(await itemsOf(), equipped, '取消: unchanged');
|
||||
assert.ok(await handItem(KIT_ITEMS[2]), '取消: the item stays in the hand');
|
||||
// ... and is drawn back on its bench slot before it is picked up again. The view keeps a dropped piece on the drop
|
||||
// tile for 1.3 s (render/app/tune.js DROP_PENDING_MS), then flies it back in a frame-timed tween: with few frames
|
||||
// (software-rendered headless Chrome on a loaded machine) the drag aimed where it stood a moment before grabbed the
|
||||
// operator under it, whose direction wheel then swallowed the retry (the 0.2.0 candidate's full pass).
|
||||
const back = await c.page.waitForFunction((uid, x, y) => {
|
||||
const r = globalThis.__SP_VIEW__?.pieceScreenRect(uid);
|
||||
return !!r && Math.abs(r.left + r.width / 2 - x) < 2 && Math.abs(r.top + r.height / 2 - y) < 2;
|
||||
}, { timeout: 15000, polling: 100 }, third.uid, benchSlot.x, benchSlot.y).then(() => true, () => false);
|
||||
assert.ok(back, '取消: the item is drawn back on its bench slot');
|
||||
|
||||
// again → pick the OLDER item (not the server's default either way: explicit replaceUid) → 确认替换
|
||||
await dropItem(third);
|
||||
|
||||
+26
-12
@@ -243,19 +243,33 @@ describe('干员调配 overlay (real server, headless Chrome)', { skip: !ENABLED
|
||||
await page.waitForSelector('.lo-top .lo-deadline', { visible: true, timeout: 3000 });
|
||||
assert.match(await page.$eval('.lo-top .lo-deadline', (el) => el.getAttribute('aria-label')), /剩余\d+秒/);
|
||||
// review: the two new 导出 / 导入 buttons must fit next to the countdown on a narrow phone in landscape (667×375).
|
||||
// 0.2.0: the third tab (自选编队) ran the right column into the tabs there, at 844×390, and in English at every size.
|
||||
// Only the size changes (toggling isMobile / hasTouch would make puppeteer reload the page).
|
||||
await page.setViewport({ width: 667, height: 375 });
|
||||
const bar = await page.evaluate(() => {
|
||||
const top = document.querySelector('.lo-top');
|
||||
const right = document.querySelector('.lo-top__right').getBoundingClientRect();
|
||||
const center = document.querySelector('.lo-top__center').getBoundingClientRect();
|
||||
const buttons = [...document.querySelectorAll('.lo-top__right .btn')].map((b) => Math.round(b.getBoundingClientRect().right));
|
||||
return { overflow: top.scrollWidth - top.clientWidth, overlap: right.left < center.right, offscreen: right.right > innerWidth, buttons };
|
||||
});
|
||||
assert.ok(bar.overflow <= 0, `the top bar does not scroll sideways (${JSON.stringify(bar)})`);
|
||||
assert.equal(bar.overlap, false, `the right column does not run into the title (${JSON.stringify(bar)})`);
|
||||
assert.equal(bar.offscreen, false, `every button ends inside the viewport (${JSON.stringify(bar)})`);
|
||||
await page.screenshot({ path: path.join(OUT, 'loadout-narrow-toolbar.png') });
|
||||
const switchLang = async (lang) => {
|
||||
await page.evaluate((l) => import('/js/ui/lang.js').then((m) => m.switchLang(l)), lang);
|
||||
await page.waitForFunction((l) => document.documentElement.lang.startsWith(l) && /[一-鿿]/.test(document.querySelector('.lo-tabs').textContent) === (l === 'zh'), { timeout: 5000 }, lang);
|
||||
};
|
||||
for (const [lang, sizes] of [['zh', [[667, 375], [844, 390]]], ['en', [[667, 375], [844, 390], [1920, 1080]]]]) {
|
||||
if (lang !== 'zh') await switchLang(lang);
|
||||
for (const [w, h] of sizes) {
|
||||
await page.setViewport({ width: w, height: h });
|
||||
const bar = await page.evaluate(() => {
|
||||
const top = document.querySelector('.lo-top');
|
||||
const tabs = document.querySelector('.lo-tabs');
|
||||
const right = document.querySelector('.lo-top__right').getBoundingClientRect();
|
||||
const center = document.querySelector('.lo-top__center').getBoundingClientRect();
|
||||
const buttons = [...document.querySelectorAll('.lo-top__right .btn')].map((b) => Math.round(b.getBoundingClientRect().right));
|
||||
return { overflow: top.scrollWidth - top.clientWidth, overlap: right.left < center.right, offscreen: right.right > innerWidth, tabsCut: tabs.scrollWidth - tabs.clientWidth, buttons };
|
||||
});
|
||||
const at = `${lang} ${w}×${h} ${JSON.stringify(bar)}`;
|
||||
assert.ok(bar.overflow <= 0, `the top bar does not scroll sideways (${at})`);
|
||||
assert.equal(bar.overlap, false, `the right column does not run into the title (${at})`);
|
||||
assert.equal(bar.offscreen, false, `every button ends inside the viewport (${at})`);
|
||||
assert.ok(bar.tabsCut <= 0, `the three tabs show in full (${at})`);
|
||||
if (w === 667) await page.screenshot({ path: path.join(OUT, `loadout-narrow-toolbar${lang === 'zh' ? '' : `-${lang}`}.png`) });
|
||||
}
|
||||
}
|
||||
await switchLang('zh');
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
await page.type('.lo-search input', '隐现');
|
||||
await page.waitForFunction(() => document.querySelectorAll('.lo-card').length === 1, { timeout: 5000 });
|
||||
|
||||
Reference in new issue
Block a user