From bc22f9cbe6af10d42e9b8699f609ad9434caa67b Mon Sep 17 00:00:00 2001 From: Xinglan233 Date: Thu, 8 Oct 2026 18:44:09 +0800 Subject: [PATCH] feedback7 G-UI: Enter on the equip-replace dialog's buttons keeps its native behaviour (PR #358 by @Xinglan233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied as-is. The replace dialog no longer treats Enter that started on a button as confirm. Enter or Space on 取消 closes the dialog and leaves the chosen equipment in place. Enter on an option only changes the selection. Enter on the confirm button still sends g.equip with that replaceUid. This matches the guard already used by DialogHost. Tests: node --test test/ui/mock.e2e.test.js test/ui/leftovers.test.js test/ui/standin-ui.test.js — 31 pass, 0 fail. The mock e2e suite skipped because SP_E2E is unset. eslint on the touched files: 0 errors (one pre-existing unused-assignment warning in mock.e2e.test.js). --- public/js/ui/equipReplace.js | 3 +- test/ui/mock.e2e.test.js | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/public/js/ui/equipReplace.js b/public/js/ui/equipReplace.js index f1021bd7..6913844f 100644 --- a/public/js/ui/equipReplace.js +++ b/public/js/ui/equipReplace.js @@ -111,7 +111,8 @@ export function EquipReplaceDialog({ request, getItem = () => null, busy = false ].join(''); const confirmIcon = localAsset('ui/battle', 'equip_replace_comfirm_icon'); const swapIcon = localAsset('ui/battle', 'equip_replace_replace_icon'); - return html`
{ if (e.key === 'Enter' && chosen && !busy) { e.preventDefault(); onConfirm(chosen.uid); } }}> + // A focused button owns Enter through its native click, including Cancel and the item options. + return html`
{ if (e.key === 'Enter' && e.target?.tagName !== 'BUTTON' && chosen && !busy) { e.preventDefault(); onConfirm(chosen.uid); } }}> <${Modal} open=${true} tone="red" class="eqr" width="min(7.6rem, 96vw)" micro="EQUIPMENT · REPLACE" title=${t('替换装备')} onClose=${onCancel} actions=${html` <${Button} variant="secondary" icon="close" class="eqr__cancel" onClick=${onCancel}>${t('取消')} diff --git a/test/ui/mock.e2e.test.js b/test/ui/mock.e2e.test.js index 6030af49..5fe57f73 100644 --- a/test/ui/mock.e2e.test.js +++ b/test/ui/mock.e2e.test.js @@ -95,6 +95,70 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set }); } + test('equip replace: focused buttons keep their native Enter action', async () => { + const { page, problems } = await open('phase=PREP', { render: 'fallback' }); + const initial = await mockState(page); + const target = initial.board.find((p) => p.kind === 'chess' && p.items?.length === 2); + assert.ok(target, 'the fixture has an operator with two equipped items'); + const item = initial.hand.find((p) => p?.kind === 'item' && !target.items.some((x) => x.id === p.id)); + assert.ok(item, 'the fixture has a loose equipment item'); + const requests = () => page.evaluate(() => globalThis.__MOCK__.S().requests.filter(([t]) => t === 'g.equip')); + const center = (sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; }); + const openReplace = async () => { + const from = await center(`.ff-piece[data-uid="${item.uid}"]`); + const to = await center(`.ff-piece[data-uid="${target.uid}"]`); + await page.mouse.move(from.x, from.y); + await page.mouse.down(); + await page.mouse.move(to.x, to.y, { steps: 8 }); + await sleep(80); + await page.mouse.up(); + await page.waitForSelector('.eqr__opt'); + await sleep(500); // let the dialog's mount effect settle before selecting an item + await page.click(`.eqr__opt[data-uid="${target.items[1].uid}"]`); + await page.waitForSelector('.eqr__opt[aria-checked="true"]'); + }; + const tabToCancel = async () => { + for (let i = 0; i < 8; i++) { + if (await page.evaluate(() => document.activeElement?.matches('.eqr__cancel'))) return; + await page.keyboard.press('Tab'); + } + assert.fail('Tab did not focus Cancel'); + }; + + for (const key of ['Space', 'Enter']) { + await openReplace(); + await tabToCancel(); + await page.keyboard.press(key); + await page.waitForFunction(() => !document.querySelector('.eqr')); + await sleep(200); + assert.deepEqual(await requests(), [], `${key} on Cancel sends no equipment intent`); + const state = await mockState(page); + assert.deepEqual(state.board.find((p) => p.uid === target.uid).items, target.items, 'both equipped items remain'); + assert.ok(state.hand.some((p) => p?.uid === item.uid), 'the new item remains in the hand'); + } + + // Enter on an equipment option toggles its selection; only the confirmation button replaces it. + await openReplace(); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => !document.querySelector('.eqr__opt[aria-checked="true"]')); + await page.keyboard.press('Enter'); + await page.waitForSelector('.eqr__opt[aria-checked="true"]'); + assert.deepEqual(await requests(), [], 'Enter on an option only changes the selected item'); + await tabToCancel(); + await page.keyboard.press('Tab'); + assert.equal(await page.evaluate(() => document.activeElement?.matches('.eqr__ok')), true); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => !document.querySelector('.eqr')); + await sleep(200); + assert.deepEqual(await requests(), [['g.equip', { itemUid: item.uid, targetUid: target.uid, replaceUid: target.items[1].uid }]]); + const replaced = await mockState(page); + assert.deepEqual(replaced.board.find((p) => p.uid === target.uid).items.map((x) => x.uid), [target.items[0].uid, item.uid]); + assert.ok(!replaced.hand.some((p) => p?.uid === item.uid), 'the confirmed equipment leaves the hand'); + // This keyboard regression also runs without optional art/fonts; JavaScript exceptions still fail it. + assert.deepEqual(problems.filter((p) => p.startsWith('pageerror:')), []); + await page.close(); + }); + test('prep: buy, shortcuts, ready, Esc', async () => { const { page, problems } = await open('phase=PREP'); let s = await mockState(page);