From 10ef14817bc1059b16eab765a070772e5f579a2f Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 18:38:20 +0800 Subject: [PATCH] =?UTF-8?q?feedback6:=20Esc=20in=20the=20=E8=87=AA?= =?UTF-8?q?=E9=80=89=20picker=20closes=20only=20the=20picker,=20like=20its?= =?UTF-8?q?=20=E5=8F=96=E6=B6=88=20(GitHub=20#284=20by=20@Xinglan233,=20id?= =?UTF-8?q?ea=20from=20PR=20#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The 干员调配 overlay's capture-phase Esc closed the whole overlay while a 自选 picker was open (it only gave way to a .modal; the picker is a .diy-pick section without its own key handling), so one Esc went back to the lobby. - DiyPicker registers its own capture-phase Escape handler (calls onClose, as 取消 and the × do; skipped while a .modal — the 导入 dialog — is open, which handles Esc first); loadout.js returns early on Escape while a .diy-pick is open. The second Esc closes the overlay as before. Saved picks are untouched (the draft is dropped, as with 取消). - test/ui/diy.e2e.test.js: a real-server browser test that needs no art (5阶 slot, then a 6阶 slot with Esc pressed in the search field): the picker closes, the overlay and its four slots stay, sp.pref.diy is unchanged, the next Esc closes the overlay. Fails on the parent (「Esc keeps the 干员调配 overlay」), passes here. - Tests: SP_E2E=1 diy.e2e (the new test) 1 / 1; client-static, i18n, ui/diy*, ui/loadout* 369 / 369; eslint clean. --- public/js/screens/diy.js | 14 +++++++++++- public/js/screens/loadout.js | 1 + test/ui/diy.e2e.test.js | 43 +++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/public/js/screens/diy.js b/public/js/screens/diy.js index 59c35ef2..381788c3 100644 --- a/public/js/screens/diy.js +++ b/public/js/screens/diy.js @@ -8,7 +8,7 @@ // /diy.js). Out of match: the next match takes the picks. The picks live in ui/loadoutSync.js (localStorage + room.diy); // the model is ui/diyModel.js. Styles: css/screens/loadout.css (diy-*). -import { useState } from '../../vendor/hooks.module.js'; +import { useEffect, 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'; @@ -141,6 +141,18 @@ export function DiyPicker(props) { const [filter, setFilter] = useState('all'); 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(() => { + const onKey = (e) => { + if (e.key !== 'Escape' || e.ctrlKey || e.metaKey || e.altKey || document.querySelector('.modal')) return; + e.preventDefault(); + e.stopImmediatePropagation(); + props.onClose(); + }; + window.addEventListener('keydown', onKey, true); + return () => window.removeEventListener('keydown', onKey, true); + }, [props.onClose]); return DiyPickerView({ ...props, filter, query, draft, onFilter: setFilter, onQuery: setQuery, onDraft: setDraft }); } diff --git a/public/js/screens/loadout.js b/public/js/screens/loadout.js index c98ab8f6..ae8e3a14 100644 --- a/public/js/screens/loadout.js +++ b/public/js/screens/loadout.js @@ -550,6 +550,7 @@ function LoadoutScreen({ st }) { const onKey = (e) => { if (e.ctrlKey || e.metaKey || e.altKey) return; if (document.querySelector('.modal')) return; // a confirm dialog handles its own keys + if (e.key === 'Escape' && document.querySelector('.diy-pick')) return; // the 自选 picker's Esc closes only the picker (diy.js) const typing = e.target && /^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName); if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); closeLoadout(); return; } if (typing) return; diff --git a/test/ui/diy.e2e.test.js b/test/ui/diy.e2e.test.js index 339781f6..3976721c 100644 --- a/test/ui/diy.e2e.test.js +++ b/test/ui/diy.e2e.test.js @@ -17,11 +17,52 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import { Client, ROOT, sleep, hasChrome, startRealServer, problemsOf } from '../e2e/client.mjs'; -const ENABLED = process.env.SP_E2E === '1' && hasChrome() && existsSync(path.join(ROOT, 'public/assets')); +const UI_ENABLED = process.env.SP_E2E === '1' && hasChrome(); +const ENABLED = UI_ENABLED && existsSync(path.join(ROOT, 'public/assets')); const SLOT = 'chess_char_5_diy1_a'; const SIEGE = 'char_112_siege'; const PICK = { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' }; +// GitHub #284 (idea from PR #286): Esc in the picker cancels only the picker, like its 取消, also from its search +// field — the 干员调配 overlay and its four slots stay, and the saved picks do not change; the next Esc closes the overlay +describe('自选编队 picker: Esc closes only the picker (real server, no art needed)', { skip: !UI_ENABLED && 'set SP_E2E=1 (and have Chrome)' }, () => { + test('5阶 / 6阶 slots: Esc cancels the picker, the overlay stays; a second Esc closes the overlay', { timeout: 90000 }, async () => { + const srv = await startRealServer(); + const P = (await import('puppeteer-core')).default; + const c = new Client(P, srv.base, 'diy-esc', { prefix: 'diy-esc' }); + const picker = '[data-testid="diy-picker"]'; + const saved = () => c.page.evaluate(() => localStorage.getItem('sp.pref.diy')); + try { + await c.open(); + await c.enter('自选取消'); + await c.click('.lobby-screen [data-testid="loadout-open"]'); + await c.page.waitForSelector('.lo .lo-tab[data-tab="diy"]', { visible: true, timeout: 15000 }); + await c.click('.lo .lo-tab[data-tab="diy"]'); + const before = await saved(); + for (const [slot, search] of [[SLOT, false], ['chess_char_6_diy1_a', true]]) { + await c.page.waitForSelector(`.diy-slot[data-slot="${slot}"] .diy-slot__fill`, { visible: true, timeout: 15000 }); + await c.click(`.diy-slot[data-slot="${slot}"] .diy-slot__fill`); + await c.page.waitForSelector(picker, { visible: true, timeout: 8000 }); + if (search) { + await c.click(`${picker} input[type="search"]`); + await c.page.keyboard.type('推进'); + } + await c.page.keyboard.press('Escape'); + await c.page.waitForSelector(picker, { hidden: true, timeout: 3000 }); + 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`); + } + 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 }); + assert.deepEqual(c.problems.filter((p) => p.startsWith('pageerror:')), []); + } finally { + await c.close(); + await srv.stop(); + } + }); +}); + describe('0.2.0 自选编队 — a slotted operator in the own shop and battle (real server)', { skip: !ENABLED && 'set SP_E2E=1 (Chrome + public/assets)' }, () => { test('自选编队: slot 推进之王 → level-5 shop card (自选) → buy → deploy → the local battle fields 推进之王', { timeout: 6 * 60 * 1000 }, async () => { const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 2, startRound: 1, kit: 0, level: 5, shop: [SLOT] } });