0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)
Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -0,0 +1,172 @@
|
||||
// Browser check of 设置 →「文字大小」 (opt-in, needs Chrome, ~30 s):
|
||||
// SP_E2E=1 CHROME_PATH=… node --test test/ui/text-scale.e2e.test.js
|
||||
//
|
||||
// A 844×390 landscape phone: 1rem is clamped at 40 px, so the .18rem body text is 7.2 CSS px. The setting steps the text
|
||||
// root `--t` (css/theme.css) instead of the layout root: the readable text grows, while `documentElement`'s font-size,
|
||||
// the modal's rem-sized box and — through the dev mock's 休整期 screen — the field host, the canvas and the shop bar stay
|
||||
// exactly where they were. Static counterpart: test/ui/text-scale.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 PHONE = { width: 844, height: 390 };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** The settings the mock harness reads: `sp.pref.settings` with one text step (the argument is passed into the page —
|
||||
* evaluateOnNewDocument cannot see a closure). */
|
||||
const seedSettings = (page, textSize) => page.evaluateOnNewDocument((ts) => {
|
||||
try { localStorage.setItem('sp.pref.settings', JSON.stringify({ textSize: ts })); } catch { /* ignore */ }
|
||||
}, textSize);
|
||||
|
||||
describe('文字大小 on a phone (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', '--force-device-scale-factor=1'] });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser?.close();
|
||||
await srv?.close();
|
||||
});
|
||||
|
||||
/** A page watching for console / page errors, with `sp.pref.settings` seeded before the first script runs. */
|
||||
async function open(url, textSize, { seed = true } = {}) {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport(PHONE);
|
||||
const problems = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
if (seed && textSize) await seedSettings(page, textSize);
|
||||
await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||
return { page, problems };
|
||||
}
|
||||
|
||||
/** What the settings modal shows at this step: the roots, a readable label, its Latin micro label, the box. */
|
||||
const modalMetrics = (page) => page.evaluate(() => {
|
||||
const px = (sel, prop = 'fontSize') => {
|
||||
const el = document.querySelector(sel);
|
||||
return el ? parseFloat(getComputedStyle(el)[prop]) : null;
|
||||
};
|
||||
const box = document.querySelector('.modal__box')?.getBoundingClientRect();
|
||||
const body = document.querySelector('.modal__body');
|
||||
const on = document.querySelector('[data-testid="text-size"] button.is-on');
|
||||
// nothing may stick out of the modal's box (the label wraps and the four steps fit its row)
|
||||
let outRight = 0;
|
||||
for (const el of document.querySelectorAll('.modal__box *')) {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (box && r.width > 0) outRight = Math.max(outRight, r.right - box.right);
|
||||
}
|
||||
return {
|
||||
root: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
||||
label: px('.set-row__label'),
|
||||
micro: px('.set-row__label .micro'),
|
||||
modalWidth: box ? Math.round(box.width) : null,
|
||||
bodyOverflowX: body ? body.scrollWidth - body.clientWidth : null,
|
||||
outRight: Math.round(outRight),
|
||||
step: on?.textContent ?? null,
|
||||
hint: document.querySelector('.set-textsize-note')?.textContent ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
test('the steps grow the readable text; the layout root and the modal box do not move', async () => {
|
||||
const { page, problems } = await open(`${base}/`, null, { seed: false });
|
||||
await page.waitForSelector('input', { timeout: 15000 });
|
||||
await page.type('input', '文字大小');
|
||||
await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /开始/.test(b.textContent))?.click());
|
||||
await page.waitForSelector('.lobby-screen', { timeout: 15000 });
|
||||
await page.click('.lobby-screen [data-testid="settings-btn"]');
|
||||
await page.waitForSelector('.modal .set-list', { timeout: 5000 });
|
||||
await sleep(200);
|
||||
|
||||
const small = await modalMetrics(page);
|
||||
assert.equal(small.step, '小', 'the default is the design\'s own sizes');
|
||||
assert.equal(small.root, 40, 'a landscape phone clamps the layout root at 40 px');
|
||||
assert.ok(Math.abs(small.label - 0.18 * 40) < 0.1, `the body text starts at the reported 7.2 px (${small.label})`);
|
||||
assert.ok(small.hint && /棋盘/.test(small.hint), 'the row says the layout is not touched');
|
||||
|
||||
const step = async (name) => {
|
||||
await page.evaluate((n) => {
|
||||
const btn = [...document.querySelectorAll('[data-testid="text-size"] button')].find((b) => b.textContent.trim() === n);
|
||||
btn?.click();
|
||||
}, name);
|
||||
await sleep(200);
|
||||
return modalMetrics(page);
|
||||
};
|
||||
|
||||
const md = await step('中');
|
||||
const lg = await step('大');
|
||||
const xl = await step('特大');
|
||||
assert.equal(md.step, '中');
|
||||
assert.equal(xl.step, '特大', 'the pick sticks');
|
||||
assert.ok(md.label > small.label * 1.5 && lg.label > md.label && xl.label > lg.label,
|
||||
`the readable text grows step by step (${[small.label, md.label, lg.label, xl.label].join(' → ')})`);
|
||||
assert.ok(Math.abs(xl.label - 0.18 * 88) < 0.2, `特大 reaches the 88 px text root (${xl.label})`);
|
||||
assert.ok(xl.micro > small.micro * 1.9, `the 4.4 px micro labels follow (${small.micro} → ${xl.micro})`);
|
||||
for (const m of [md, lg, xl]) {
|
||||
assert.equal(m.root, 40, 'the layout root never moves: the field camera and the DOM board read 1rem');
|
||||
assert.equal(m.modalWidth, small.modalWidth, 'the modal\'s box is sized in rem and does not change');
|
||||
assert.ok(m.bodyOverflowX <= 1, `the rows do not overflow the modal body (${m.bodyOverflowX} px)`);
|
||||
assert.ok(m.outRight <= 1, `nothing sticks out of the modal's box (${m.outRight} px): the label wraps and the four steps fit`);
|
||||
}
|
||||
|
||||
// saved with the settings and restored on the next visit
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('sp.pref.settings')).textSize), 'xl');
|
||||
await page.reload({ waitUntil: 'networkidle0' });
|
||||
await page.waitForSelector('.lobby-screen', { timeout: 15000 });
|
||||
await page.click('.lobby-screen [data-testid="settings-btn"]');
|
||||
await page.waitForSelector('.modal .set-list', { timeout: 5000 });
|
||||
const again = await modalMetrics(page);
|
||||
assert.equal(again.step, '特大', 'the choice survives a reload');
|
||||
assert.equal(again.root, 40);
|
||||
assert.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('the in-match field, canvas and shop bar do not move; the settings dialog in a match grows', async () => {
|
||||
const measure = async (textSize) => {
|
||||
const { page, problems } = await open(`${base}/dev/game-mock.html?shot=1&phase=PREP&variant=settings`, textSize);
|
||||
await page.waitForFunction(() => !!document.querySelector('.screen:not(.gload)'), { timeout: 15000 });
|
||||
await sleep(900);
|
||||
const m = await page.evaluate(() => {
|
||||
const box = (sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)];
|
||||
};
|
||||
return {
|
||||
root: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
||||
field: box('.gm__field'),
|
||||
canvas: box('.gm__field canvas'),
|
||||
shopbar: box('.gm__hud .shopbar'),
|
||||
label: (() => { const el = document.querySelector('.set-row__label'); return el ? parseFloat(getComputedStyle(el).fontSize) : null; })(),
|
||||
};
|
||||
});
|
||||
return { ...m, problems, page };
|
||||
};
|
||||
|
||||
const small = await measure('sm');
|
||||
const big = await measure('xl');
|
||||
assert.ok(small.field && small.shopbar, 'the prep screen with its HUD is on screen');
|
||||
assert.equal(small.root, 40);
|
||||
assert.equal(big.root, 40, 'the text step never reaches the root');
|
||||
assert.deepEqual(big.field, small.field, 'the field host (host.clientWidth / clientHeight) is unchanged');
|
||||
assert.deepEqual(big.canvas, small.canvas, 'and so is the canvas the renderer resizes to it');
|
||||
assert.deepEqual(big.shopbar, small.shopbar, 'the shop bar is a fixed HUD box: still the design\'s size');
|
||||
assert.ok(big.label > small.label * 1.9, `while the dialog's readable text grows (${small.label} → ${big.label})`);
|
||||
assert.deepEqual(small.problems, []);
|
||||
assert.deepEqual(big.problems, []);
|
||||
await small.page.close();
|
||||
await big.page.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user