From 5952539046770320c432979f567a1c41c6b98561 Mon Sep 17 00:00:00 2001 From: Xinglan233 Date: Thu, 8 Oct 2026 18:43:40 +0800 Subject: [PATCH] =?UTF-8?q?feedback7=20G-UI:=20clear=20the=20=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E4=B8=AD=E5=BF=83=20upgrade=20confirm=20for=20the=20r?= =?UTF-8?q?eached=20level=20after=20pressing=20D=20(PR=20#356=20by=20@Xing?= =?UTF-8?q?lan233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied as-is. The shop bar's two-tap arm for the level button is now lv:. A successful upgrade, including the D shortcut, changes the level and drops that arm, so the next click only selects the new level. A freeze at the same level keeps the confirm. Another card's arm is left alone, and D still upgrades immediately. Tests: node --test test/ui/mock.e2e.test.js test/ui/playtest6-elite.test.js test/ui/playtest6-ui.test.js — 20 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/shopBar.js | 10 +++--- test/ui/mock.e2e.test.js | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/public/js/ui/shopBar.js b/public/js/ui/shopBar.js index e8a275c2..d001b15e 100644 --- a/public/js/ui/shopBar.js +++ b/public/js/ui/shopBar.js @@ -196,7 +196,7 @@ function LevelCard({ shop, reason, armed = false, onTap }) { export const armKey = (kind, idx, slot) => `${kind}:${idx}:${slot?.id ?? ''}`; /** - * The slot an armed key names (`c` / `i` shop slots, `r` reward slots), or null ('lv', nothing armed, a stale key). + * The slot an armed key names (`c` / `i` shop slots, `r` reward slots), or null (a level card, nothing armed, a stale key). * @param {string|null} key * @param {any[]} slots the shop's slots * @param {any[]|null} rewardSlots the shown reward offer's slots @@ -288,10 +288,12 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel, const free = Number(shop.freeRefreshes) || 0; const showReward = !!(reward && Array.isArray(reward.slots) && reward.slots.length); + // A completed upgrade changes the level, so its confirmation cannot carry into the next upgrade. + const levelKey = `lv:${shop.level ?? 1}`; // two-tap: the keys that may stay armed right now const keys = new Set(); if (!collapsed) { - if (!lvReason) keys.add('lv'); + if (!lvReason) keys.add(levelKey); slots.forEach((s, i) => { if (s && !s.sold) keys.add(armKey(s.kind === 'item' ? 'i' : 'c', i, s)); }); if (showReward) reward.slots.forEach((s, i) => { if (s && !s.sold) keys.add(armKey('r', i, s)); }); } @@ -312,7 +314,7 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel, buy(idx); }; const tapLevel = () => { - if (armed !== 'lv') { setArmed('lv'); return; } + if (armed !== levelKey) { setArmed(levelKey); return; } setArmed(null); onLevel(); }; @@ -341,7 +343,7 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel,
- <${LevelCard} shop=${shop} reason=${lvReason} armed=${armed === 'lv'} onTap=${tapLevel} /> + <${LevelCard} shop=${shop} reason=${lvReason} armed=${armed === levelKey} onTap=${tapLevel} /> ${showReward ? html`<${RewardCards} offer=${reward} priv=${priv} editable=${editable} onPick=${onReward} onDetail=${onDetail} onLater=${onRewardLater} armed=${armed} onTap=${tapCard} offBonds=${offBonds} />` : html`
diff --git a/test/ui/mock.e2e.test.js b/test/ui/mock.e2e.test.js index ed87113e..6030af49 100644 --- a/test/ui/mock.e2e.test.js +++ b/test/ui/mock.e2e.test.js @@ -149,6 +149,77 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set await page.close(); }); + test('prep: a shortcut upgrade clears the previous level confirmation', async () => { + const { page, problems } = await open('phase=PREP', { render: 'fallback' }); + try { + await page.evaluate(() => globalThis.__MOCK__.mutate((S) => { S.priv.funds = 100; })); + const before = await mockState(page); + const pressed = () => page.$eval('.shopbar .lvcard', (el) => el.getAttribute('aria-pressed')); + await page.click('.shopbar .lvcard'); + await page.waitForFunction(() => document.querySelector('.lvcard').getAttribute('aria-pressed') === 'true'); + assert.equal((await mockState(page)).funds, before.funds, 'selecting does not charge'); + await page.keyboard.press('KeyD'); + await page.waitForFunction((lv) => globalThis.__MOCK__.S().priv.shop.level === lv + 1, {}, before.shop.level); + const upgraded = await mockState(page); + assert.equal(upgraded.funds, before.funds - before.shop.upgradePrice); + assert.equal(await pressed(), 'false', 'the completed upgrade must disarm its confirmation'); + await page.click('.shopbar .lvcard'); + await page.waitForFunction(() => document.querySelector('.lvcard').getAttribute('aria-pressed') === 'true'); + await sleep(300); // allow a mistaken second upgrade to reach the mock server + assert.equal((await mockState(page)).shop.level, upgraded.shop.level, 'the next click only selects'); + assert.equal((await mockState(page)).funds, upgraded.funds, 'the next click does not charge'); + await page.keyboard.press('Escape'); + await page.waitForFunction(() => document.querySelector('.lvcard').getAttribute('aria-pressed') === 'false'); + await page.keyboard.press('KeyD'); + await page.waitForFunction((lv) => globalThis.__MOCK__.S().priv.shop.level === lv + 1, {}, upgraded.shop.level); + assert.equal((await mockState(page)).funds, upgraded.funds - upgraded.shop.upgradePrice, 'D still upgrades directly'); + // Optional art / audio may be absent; this state regression needs no asset pack. + assert.deepEqual(problems.filter((p) => p.startsWith('pageerror:')), []); + } finally { await page.close(); } + }); + + test('prep: mouse level confirmation survives same-level updates and resets after purchase', async () => { + const { page, problems } = await open('phase=PREP', { render: 'fallback' }); + try { + await page.evaluate(() => globalThis.__MOCK__.mutate((S) => { S.priv.funds = 100; })); + const before = await mockState(page); + await page.click('.shopbar .lvcard'); + await page.waitForFunction(() => document.querySelector('.lvcard').getAttribute('aria-pressed') === 'true'); + await page.keyboard.press('KeyF'); + await page.waitForFunction(() => globalThis.__MOCK__.S().priv.shop.frozen); + assert.equal(await page.$eval('.lvcard', (el) => el.getAttribute('aria-pressed')), 'true', 'an unrelated shop update keeps the selection'); + await page.click('.shopbar .lvcard'); + await page.waitForFunction((lv) => globalThis.__MOCK__.S().priv.shop.level === lv + 1, {}, before.shop.level); + const upgraded = await mockState(page); + assert.equal(upgraded.funds, before.funds - before.shop.upgradePrice); + assert.equal(await page.$eval('.lvcard', (el) => el.getAttribute('aria-pressed')), 'false'); + await page.click('.shopbar .lvcard'); + await page.waitForFunction(() => document.querySelector('.lvcard').getAttribute('aria-pressed') === 'true'); + await sleep(300); + assert.equal((await mockState(page)).shop.level, upgraded.shop.level); + assert.equal((await mockState(page)).funds, upgraded.funds); + assert.deepEqual(problems.filter((p) => p.startsWith('pageerror:')), []); + } finally { await page.close(); } + }); + + test('prep: a level change preserves another shop card selection', async () => { + const { page, problems } = await open('phase=PREP', { render: 'fallback' }); + try { + await page.evaluate(() => globalThis.__MOCK__.mutate((S) => { S.priv.funds = 100; })); + const before = await mockState(page); + await page.click('.shopbar__cards .scard:not(.scard--sold)'); + await page.waitForSelector('.shopbar__cards .scard.is-armed'); + const selected = await page.$eval('.shopbar__cards .scard.is-armed', (el) => el.getAttribute('aria-label')); + await page.keyboard.press('KeyD'); + await page.waitForFunction((lv) => globalThis.__MOCK__.S().priv.shop.level === lv + 1, {}, before.shop.level); + assert.equal(await page.$eval('.shopbar__cards .scard.is-armed', (el) => el.getAttribute('aria-label')), selected); + assert.equal(await page.$eval('.lvcard', (el) => el.getAttribute('aria-pressed')), 'false'); + await page.keyboard.press('Escape'); + await page.waitForSelector('.scard.is-armed', { hidden: true }); + assert.deepEqual(problems.filter((p) => p.startsWith('pageerror:')), []); + } finally { await page.close(); } + }); + test('prep: Q retreats and X sells only the selected operator', async () => { const { page, problems } = await open('phase=PREP', { render: 'fallback' }); const initial = await mockState(page);