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