- 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.
121 lines
8.4 KiB
JavaScript
121 lines
8.4 KiB
JavaScript
// 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));
|
||
});
|
||
});
|