Files
sganggs 221e6b8dd2 0.2.5-dev: community round 30 and follow-ups
- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently).
- Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision).
- 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified).
- Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage.
- Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue.
- Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia).
- golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
2026-10-12 14:07:08 +08:00

85 lines
4.6 KiB
JavaScript

// test/ui/refresh-tap.e2e.test.js — GitHub #501 (single-ptilopsis): 高延迟下连按「刷新」只发送一次请求. Through the mock
// harness (public/dev/game-mock.html) in headless Chrome, with the mock's reply to g.refresh delayed: the first tap
// sends one request and dims the button, a spam tap and the R key while it is on the wire send nothing, the shop pays
// once, and the next tap after the reply refreshes again. Opt-in: SP_E2E=1 (needs Chrome; CHROME_PATH overrides).
//
// SP_E2E=1 node --test test/ui/refresh-tap.e2e.test.js
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const BTN = '.shopbar__tools .toolbtn--amber';
describe('the shop refresh single-flights (GitHub #501, mock harness, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
let srv;
let browser;
let base;
before(async () => {
const { startServer } = await import('../../server/index.js');
const puppeteer = (await import('puppeteer-core')).default;
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
base = `http://127.0.0.1:${srv.port}`;
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] });
});
after(async () => {
await browser?.close();
await srv?.close();
});
test('a tap under a lagging shop pays once: the button dims, the extra tap / R key are dropped, the next tap works', async () => {
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.goto(`${base}/dev/game-mock.html?shot=1&phase=PREP&render=fallback`, { waitUntil: 'networkidle0' });
await page.waitForFunction(() => !!document.querySelector('.screen:not(.gload)') && !!globalThis.__SP_VIEW__, { timeout: 20000 });
// the mock answers instantly; give g.refresh a high RTT, and count what the client actually sends (gameActions.js
// looks net.request up at call time; the mock's own request record only sees the request after the delay)
await page.evaluate(async () => {
const { net } = await import('/js/net.js');
const orig = net.request.bind(net);
globalThis.__REFRESHES__ = 0;
net.request = (t, ...rest) => {
if (t === 'g.refresh') globalThis.__REFRESHES__ += 1;
return new Promise((res, rej) => {
setTimeout(() => orig(t, ...rest).then(res, rej), t === 'g.refresh' ? 700 : 0);
});
};
});
const btnState = () => page.evaluate(() => {
const b = document.querySelector('.shopbar__tools .toolbtn--amber');
return { disabled: !!b?.disabled, title: b?.title || null };
});
const refreshes = () => page.evaluate(() => globalThis.__REFRESHES__);
const funds = () => page.evaluate(() => globalThis.__MOCK__.S().priv.funds);
assert.equal((await btnState()).disabled, false, 'the 刷新 button starts enabled');
const n0 = await refreshes();
const f0 = await funds();
await page.click(BTN);
await sleep(150);
assert.equal(await refreshes(), n0 + 1, 'the tap sent one g.refresh');
assert.deepEqual(await btnState(), { disabled: true, title: '正在刷新…' }, 'the button dims and says it is waiting');
// the spam tap (the disabled button swallows clicks) and the R hotkey while pending: neither reaches the socket
await page.evaluate(() => document.querySelector('.shopbar__tools .toolbtn--amber')?.click());
await page.keyboard.press('r');
await sleep(200);
assert.equal(await refreshes(), n0 + 1, 'nothing else was sent');
await page.waitForFunction(() => !document.querySelector('.shopbar__tools .toolbtn--amber')?.disabled, { timeout: 5000 });
const back = await btnState();
assert.equal(back.disabled, false, 'the button is enabled again after the reply');
assert.match(back.title, /刷新商店/, 'the tooltip is back to the hotkey hint');
assert.equal(await funds(), f0 - 1, 'the shop paid exactly one refresh');
await page.click(BTN);
await sleep(150);
assert.equal(await refreshes(), n0 + 2, 'a refresh after the reply is sent again');
await page.waitForFunction(() => !document.querySelector('.shopbar__tools .toolbtn--amber')?.disabled, { timeout: 5000 });
assert.equal(await funds(), f0 - 2, 'and paid once more');
await page.close();
});
});