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);