diff --git a/public/css/theme.css b/public/css/theme.css index d58dd33c..25ab8b68 100644 --- a/public/css/theme.css +++ b/public/css/theme.css @@ -95,7 +95,7 @@ /* layers */ --z-screen: 1; --z-banner: 40; - --z-conn: 60; + --z-conn: 75; --z-modal: 80; --z-toast: 90; --z-tooltip: 95; diff --git a/test/ui/mock.e2e.test.js b/test/ui/mock.e2e.test.js index b8147a48..b612570d 100644 --- a/test/ui/mock.e2e.test.js +++ b/test/ui/mock.e2e.test.js @@ -910,6 +910,40 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set }); } + test('session replacement: recovery action stays clickable above the AI away overlay', async () => { + const { page, problems } = await open('phase=PREP', { render: 'fallback' }); + await page.click('.gtop__exit'); + await page.waitForSelector('.modal'); + const buttons = await page.$$('.modal__actions .btn'); + await buttons[1].click(); // 暂离 + await page.waitForSelector('.awayov[aria-label="AI 托管中"]', { visible: true }); + await page.evaluate(async () => { + const { net } = await import('/js/net.js'); + globalThis.__RECOVERY_CONNECTS__ = 0; + net.connect = () => { globalThis.__RECOVERY_CONNECTS__++; }; + globalThis.__MOCK__.store.set({ connection: { status: 'closed', ping: 0, attempt: 0, retryAt: 0, lastError: { code: 'REPLACED' }, everOnline: true } }); + }); + await page.waitForSelector('.conn-banner .btn', { visible: true }); + await sleep(300); + assert.deepEqual(await page.evaluate(() => { + const conn = globalThis.__MOCK__.store.get().connection; + return [conn.status, conn.lastError.code]; + }), ['closed', 'REPLACED']); + assert.ok(await page.$('.awayov'), 'the away overlay remains during recovery'); + assert.match(await page.$eval('.conn-banner', (el) => el.textContent), /该身份已在其他页面登录/); + assert.equal(await page.$eval('.conn-banner .btn', (el) => el.textContent.trim()), '在此页面继续'); + const center = await page.$eval('.conn-banner .btn', (btn) => { + const r = btn.getBoundingClientRect(); + const x = r.left + r.width / 2, y = r.top + r.height / 2; + return { x, y, hit: btn.contains(document.elementFromPoint(x, y)) }; + }); + assert.ok(center.hit, 'the recovery button receives pointer input above the away overlay'); + await page.mouse.click(center.x, center.y); + assert.equal(await page.evaluate(() => globalThis.__RECOVERY_CONNECTS__), 1, 'the pointer click invokes the recovery action'); + assert.deepEqual(problems.filter((p) => p.startsWith('pageerror:')), []); + await page.close(); + }); + test('watch: the other pair of the Final Assault is refused and the eye stays put', async () => { const { page, problems } = await open('phase=FINAL_ASSAULT'); const rows = await page.$$('.team__row');