feedback7: merge fb7-e2efix — an Esc pressed the moment the 自选 picker opens closes it (the listener comes with the picker's commit); the loadout / loadout-battle / teammate-loadout browser tests follow the 0.2.2 干员调配 (the row's quick skill / module choices, the default 练度 on the detail card, the detail back at the top before a click); golden: unchanged

This commit is contained in:
sganggs committed 2026-10-08 23:49:04 +08:00
commit b2d33b1d41
5 files changed
+41 -10

No files matched your search

+5 -3
View File
@@ -9,7 +9,7 @@
// the model is ui/diyModel.js. Styles: css/screens/loadout.css (diy-*). An owned pick's card carries its operator's 潜能 /
// 练度 selects (0.2.2, screens/cultivation.js — the same per-operator settings as 干员调配, `ops`); a prototype has neither.
import { useEffect, useState } from '../../vendor/hooks.module.js';
import { useLayoutEffect, useState } from '../../vendor/hooks.module.js';
import { html, Icon, Button, TierChip } from '../ui/components.js';
import { Img, RichText, BondGlyph } from '../ui/gameComponents.js';
import { chessAvatarUrl, chessPortraitUrl, profIconUrl, skillRecordIconUrl, moduleTypeIconUrl } from '../ui/assetUrls.js';
@@ -146,8 +146,10 @@ export function DiyPicker(props) {
const [query, setQuery] = useState('');
const [draft, setDraft] = useState(cur ? { ...cur } : null);
// Esc cancels only the picker, like its 取消 (GitHub #284, idea from PR #286); the 干员调配 overlay's own Esc skips
// while a picker is open, and a dialog over the picker (导入) still takes Esc first
useEffect(() => {
// while a picker is open, and a dialog over the picker (导入) still takes Esc first. A layout effect: the listener is
// attached in the same commit as the picker (a plain effect waits for the next frame, and an Esc pressed in between
// was swallowed — the overlay skipped it, the picker did not hear it yet; the 0.2.2 full browser pass)
useLayoutEffect(() => {
const onKey = (e) => {
if (e.key !== 'Escape' || e.ctrlKey || e.metaKey || e.altKey || document.querySelector('.modal')) return;
e.preventDefault();
+13
View File
@@ -134,6 +134,19 @@ describe('自选编队 picker: Esc closes only the picker (real server, no art n
assert.ok(await c.page.$('.lo'), `${slot}: Esc keeps the 干员调配 overlay${search ? ' (from the search field)' : ''}`);
assert.equal((await c.page.$$('.diy-slot')).length, 4, `${slot}: the four slots stay`);
}
// an Esc pressed the moment the picker appears — before the next animation frame — closes it too: its listener comes
// with the picker's own commit (a plain effect attached it a frame later and the overlay skipped the key: the
// picker stayed open — the 0.2.2 full browser pass)
const opened = await c.page.evaluate(async (slot, sel) => {
document.querySelector(`.diy-slot[data-slot="${slot}"] .diy-slot__fill`).click();
for (let k = 0; k < 50 && !document.querySelector(sel); k++) await Promise.resolve();
const open = !!document.querySelector(sel);
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
return open;
}, SLOT, picker);
assert.ok(opened, 'the picker opened');
await c.page.waitForSelector(picker, { hidden: true, timeout: 3000 });
assert.ok(await c.page.$('.lo'), 'an immediate Esc keeps the 干员调配 overlay');
assert.equal(await saved(), before, 'cancelling a picker does not change the saved picks');
await c.page.keyboard.press('Escape');
await c.page.waitForFunction(() => !document.querySelector('.lo'), { timeout: 3000 });
+12 -5
View File
@@ -53,9 +53,13 @@ describe('user playtest #2 item 1 — loadout chosen in the UI fights in the loc
await c.page.keyboard.type('野鬃');
await c.page.waitForFunction(() => document.querySelectorAll('.lo-card').length === 1, { timeout: 5000 });
await c.click('.lo-card__pick');
await c.click('.lo-detail .lo-skill[data-skill="0"]');
await c.click('.lo-detail .lo-mod[data-module="none"]');
await c.page.waitForFunction(() => document.querySelector('.lo-skill.is-on[data-skill="0"]') && document.querySelector('.lo-mod.is-on[data-module="none"]'), { timeout: 3000 });
// the row's quick choices (0.2.2: the roster is one list, a row per operator with its skills and the elite's
// modules; the detail's own module cards sit below the fold) — the row and the detail agree on S1 + 不装备
await c.click('.lo-card .lo-q--skill[data-skill="0"]');
await c.click('.lo-card .lo-q--mod[data-module="none"]');
await c.page.waitForFunction(() => document.querySelector('.lo-card .lo-q--skill[data-skill="0"]')?.getAttribute('aria-pressed') === 'true'
&& document.querySelector('.lo-card .lo-q--mod[data-module="none"]')?.getAttribute('aria-pressed') === 'true'
&& document.querySelector('.lo-detail .lo-skill.is-on[data-skill="0"]') && document.querySelector('.lo-detail .lo-mod.is-on[data-module="none"]'), { timeout: 3000 });
await c.page.waitForFunction(() => /已同步/.test(document.querySelector('.lo-sync')?.textContent || ''), { timeout: 8000 });
await c.shot('overlay');
await c.page.keyboard.press('Escape');
@@ -116,8 +120,11 @@ describe('user playtest #2 item 1 — loadout chosen in the UI fights in the loc
assert.ok(detail, 'the detail card opens for the placed elite');
assert.ok(detail.skill.includes(s1.name) && detail.tag, `detail card: ${s1.name} 已调配 (${JSON.stringify(detail)})`);
assert.ok(detail.none && detail.module.includes('未装备模组'), `detail card: 未装备模组 (${JSON.stringify(detail)})`);
// …and the stats / 特性 the unit fights with: no module (ATK 524, interval 1.00 s, "获得1点") — not the default module's
assert.equal(detail.stats['攻击'], String(rec.statsBase.atk), `detail card ATK without the module (${JSON.stringify(detail.stats)})`);
// …and the stats / 特性 the unit fights with: no module (ATK 524, interval 1.00 s, "获得1点") — not the default module's;
// 0.2.2: the card's base value carries the player's 练度, here the default 精英2 Lv.60 (effects.json aceffect_char_4,
// ×1.1 ATK — a multiplier of its own, the unit's def keeps the record's 524)
const t4 = JSON.parse(readFileSync(path.join(ROOT, 'data/effects.json'), 'utf8')).aceffect_char_4.buffs.find((b) => b.key === 'char_attribute_mul').bb;
assert.equal(detail.stats['攻击'], String(Math.round(rec.statsBase.atk * t4.atk)), `detail card ATK without the module, at the default 练度 (${JSON.stringify(detail.stats)})`);
assert.equal(detail.stats['攻击间隔'], '1.00s', 'detail card interval: ASPD 100');
assert.match(detail.trait, /获得1点部署费用/, `detail card 特性 without the module (${detail.trait})`);
await c.shot('detail');
+4
View File
@@ -455,6 +455,10 @@ describe('干员调配 overlay (real server, headless Chrome)', { skip: !ENABLED
await page.type('.lo-search input', name);
await page.waitForFunction((n) => document.querySelectorAll('.lo-card').length === 1 && document.querySelector('.lo-card .lo-card__name')?.textContent === n, { timeout: 5000 }, name);
await page.click('.lo-card__pick');
// the detail shows the picked chess and its body is back at the top: screens/loadout.js resets the scroll for a new
// chess after paint, and a click that puppeteer scrolled into view before that reset lands outside the viewport (the
// 0.2.2 detail opens with 潜能与练度 and 特质, so 局内数值 / 模组 sit below the fold at 1920 × 1080)
await page.waitForFunction((n) => document.querySelector('.lo-dhead__name')?.textContent === n && document.querySelector('.lo-detail__body')?.scrollTop === 0, { timeout: 5000 }, name);
await page.waitForSelector('.lo-sec--stats .dstat', { visible: true });
}
+7 -2
View File
@@ -59,8 +59,13 @@ describe('DESIGN §16 — a teammate\'s unit shows its owner\'s loadout (real se
await guest.page.keyboard.type('野鬃');
await guest.page.waitForFunction(() => document.querySelectorAll('.lo-card').length === 1, { timeout: 5000 });
await guest.click('.lo-card__pick');
await guest.click('.lo-detail .lo-skill[data-skill="0"]');
await guest.click('.lo-detail .lo-mod[data-module="none"]');
// the row's quick choices (0.2.2: the roster is one list, a row per operator with its skills and the elite's
// modules; the detail's own module cards sit below the fold) — the row and the detail agree on S1 + 不装备
await guest.click('.lo-card .lo-q--skill[data-skill="0"]');
await guest.click('.lo-card .lo-q--mod[data-module="none"]');
await guest.page.waitForFunction(() => document.querySelector('.lo-card .lo-q--skill[data-skill="0"]')?.getAttribute('aria-pressed') === 'true'
&& document.querySelector('.lo-card .lo-q--mod[data-module="none"]')?.getAttribute('aria-pressed') === 'true'
&& document.querySelector('.lo-detail .lo-skill.is-on[data-skill="0"]') && document.querySelector('.lo-detail .lo-mod.is-on[data-module="none"]'), { timeout: 3000 });
await guest.page.waitForFunction(() => /已同步/.test(document.querySelector('.lo-sync')?.textContent || ''), { timeout: 8000 });
await guest.page.keyboard.press('Escape');
await guest.page.waitForFunction(() => !document.querySelector('.lo'), { timeout: 3000 });