Files
ark-stronghold-protocol/test/ui/round30-tools.e2e.test.js
T
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

121 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Isolated component/tool fixtures, with production modules and CSS in a real browser.
import { test, before, after, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { CHROME, hasChrome } from '../e2e/client.mjs';
describe('round30 tool and compound-tooltip browser boundaries', { skip: !(process.env.SP_E2E === '1' && hasChrome()) }, () => {
let srv, browser, page;
before(async () => {
const { startServer } = await import('../../server/index.js');
srv = await startServer({ host: '127.0.0.1', port: 0, quiet: true });
const P = (await import('puppeteer-core')).default;
browser = await P.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox', '--disable-background-timer-throttling'] });
page = await browser.newPage();
page.on('pageerror', e => console.error('fixture page error:', e.message));
});
after(async () => { await browser?.close(); await srv?.close(); });
async function blank(query = '') {
await page.goto(`http://127.0.0.1:${srv.port}/dev/simcheck/ui.js${query}`);
await page.setContent('<html><head><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/components.css"></head><body><main id="fixture"></main></body></html>');
}
async function tooltip(width, height, anchor = '50%', touch = false) {
await page.setViewport({ width, height, hasTouch: touch }); await blank();
await page.evaluate(async (anchor) => {
const { h, render } = await import('/vendor/preact.module.js');
const { Tooltip, TooltipLayer } = await import('/js/ui/components.js');
document.documentElement.style.cssText = 'font-size:100px;--t:130px';
const root = document.getElementById('fixture');
root.style.cssText = `position:fixed;top:45%;left:${anchor};font-size:16px`;
window.changeTip = (names = 'A', aside = true) => render(h('div', {},
h(Tooltip, { text: 'same count: 12', aside: aside ? h('div', {}, ...Array.from({ length: 12 }, (_, i) => h('div', {}, `${names}-${i} Long operator name`))) : null }, h('button', { id: 'anchor' }, 'bond')),
h(TooltipLayer)), root);
window.changeTip();
}, anchor);
await page.hover('#anchor'); await page.waitForSelector('.tooltip.is-shown');
}
for (const [w, h] of [[1440, 900], [390, 844], [844, 390]]) test(`compound tooltip fits ${w}×${h}, all anchor positions`, async () => {
for (const anchor of ['1%', '50%', '90%']) {
await tooltip(w, h, anchor);
const box = await page.$eval('.tooltip', e => {
const a = e.getBoundingClientRect(), main = e.querySelector('.tooltip__main').getBoundingClientRect(), side = e.querySelector('.tooltip--aside').getBoundingClientRect();
return { left: a.left, right: a.right, top: a.top, bottom: a.bottom, overlap: main.left < side.right && side.left < main.right && main.top < side.bottom && side.top < main.bottom,
overflow: e.scrollHeight > e.clientHeight, canScroll: getComputedStyle(e).overflowY === 'auto' && getComputedStyle(e).pointerEvents === 'auto' };
});
assert.ok(box.left >= 7 && box.right <= w - 7 && box.top >= 3 && box.bottom <= h - 3, JSON.stringify(box));
assert.equal(box.overlap, false); if (box.overflow) assert.equal(box.canScroll, true);
await page.keyboard.press('Escape'); await page.waitForSelector('.tooltip', { hidden: true });
}
});
test('visible tooltip updates equal-count member lists and removes an absent aside', async () => {
await tooltip(800, 600);
await page.evaluate(() => window.changeTip('B'));
await page.waitForFunction(() => document.querySelector('.tooltip--aside')?.textContent.includes('B-11'));
assert.equal(await page.$eval('.tooltip', e => e.textContent.includes('A-')), false);
await page.evaluate(() => window.changeTip('B', false));
await page.waitForSelector('.tooltip--aside', { hidden: true });
await page.mouse.move(1, 1); await page.hover('#anchor'); await page.waitForSelector('.tooltip.is-shown');
assert.equal(await page.$eval('.tooltip', e => getComputedStyle(e).pointerEvents), 'none');
});
test('touch long press keeps the long list scrollable and outside tap closes it', async () => {
await tooltip(390, 260, '40%', true);
await page.keyboard.press('Escape'); await page.$eval('#anchor', e => e.blur());
const rect = await page.$eval('#anchor', e => { const r = e.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; });
await page.touchscreen.touchStart(rect.x, rect.y);
await page.waitForSelector('.tooltip.is-shown'); await page.touchscreen.touchEnd();
await new Promise(r => setTimeout(r, 1750));
assert.ok(await page.$('.tooltip.is-shown'));
await page.focus('.tooltip'); await page.keyboard.press('End');
await page.waitForFunction(() => document.querySelector('.tooltip')?.scrollTop > 0);
await page.touchscreen.tap(2, 2); await page.waitForSelector('.tooltip', { hidden: true });
});
const toolHtml = readFileSync(new URL('../../public/dev/sim-check.html', import.meta.url), 'utf8').replace(/<script[\s\S]*?<\/script>/g, '');
async function tool({ mode = '', query = '', count = 1 } = {}) {
await blank(query); await page.setContent(toolHtml);
await page.evaluate(async ({ mode, count }) => {
const { mountCheck } = await import('/dev/simcheck/ui.js');
const { trace } = await import('/dev/simcheck/core.js');
const scenarios = Array.from({ length: count }, (_, i) => ({ id: `case-${i}`, family: 'fast' }));
window.failAt = mode;
const adapter = {
start: () => { if (window.failAt === 'start') throw Error('start failure'); return { tick: 0 }; },
step: r => { if (window.failAt === 'step') throw Error('step failure'); return ++r.tick <= 2; },
state: r => [r.tick + (window.failAt === 'diff' ? 1 : 0)],
result: () => { if (window.failAt === 'result') throw Error('result failure'); return 'ok'; },
};
const saved = window.failAt; window.failAt = '';
const reference = { every: 1, scenarios: Object.fromEntries(scenarios.map(s => [s.id, trace(adapter, s, { every: 1 })])) };
window.failAt = saved;
mountCheck(document.getElementById('app'), { project: 'test', title: 'check', subtitle: '<img src=x onerror="window.injected=true">', adapter, scenarios,
subsets: [{ id: 'fast', label: 'fast', pick: () => true }], reference });
}, { mode, count });
}
for (const mode of ['start', 'step', 'result']) test(`sim check recovers from ${mode} exceptions and allows rerun`, async () => {
await tool({ mode }); await page.click('.sc-run');
await page.waitForFunction(() => window.__simcheck?.status === 'error');
assert.equal(await page.$eval('.sc-run', b => b.disabled), false);
assert.equal(await page.evaluate(() => !!window.injected), false);
await page.evaluate(() => { window.failAt = ''; }); await page.click('.sc-run');
await page.waitForFunction(() => window.__simcheck?.status === 'done');
assert.deepEqual(await page.evaluate(() => [window.__simcheck.expected, window.__simcheck.completed, window.__simcheck.summary.ok]), [1, 1, 1]);
});
for (const query of ['?only=typo', '?subset=typo']) test(`sim check rejects empty or unknown selection ${query}`, async () => {
await tool({ query }); await page.click('.sc-run');
await page.waitForFunction(() => window.__simcheck?.status === 'error');
assert.equal(await page.$('.sc-allok'), null);
});
test('long difference list scrolls to its report; a failed dump restores its button', async () => {
await page.setViewport({ width: 390, height: 400 }); await tool({ mode: 'diff', count: 30 });
await page.click('.sc-run'); await page.waitForFunction(() => window.__simcheck?.status === 'done');
assert.equal(await page.$$eval('.sc-list li', rows => rows.length), 30);
await page.evaluate(() => { window.failAt = 'start'; }); await page.click('.sc-list button');
await page.waitForFunction(() => document.querySelector('.sc-status').textContent.includes('start failure'));
assert.equal(await page.$eval('.sc-list button', b => b.disabled), false);
await page.$eval('.sc-report', e => e.scrollIntoView());
assert.ok(await page.evaluate(() => window.scrollY > 0));
assert.ok(await page.$eval('.sc-report', e => e.getBoundingClientRect().top < window.innerHeight));
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth));
});
});