0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)
Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -0,0 +1,146 @@
|
||||
// Real browser / server checks for local identity recovery, per-operator voice, install offers and large text.
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync } from 'node:fs';
|
||||
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
|
||||
describe('0.2.3 follow-up client features', { skip: !ENABLED }, () => {
|
||||
let server, browser, base;
|
||||
before(async () => {
|
||||
const { startServer } = await import('../../server/index.js');
|
||||
const puppeteer = (await import('puppeteer-core')).default;
|
||||
server = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
base = `http://127.0.0.1:${server.port}`;
|
||||
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] });
|
||||
});
|
||||
after(async () => { await browser?.close(); await server?.close(); });
|
||||
async function pageIn(ctx, phone = false) {
|
||||
const page = await ctx.newPage();
|
||||
await page.setViewport({ width: phone ? 844 : 1440, height: phone ? 390 : 900 });
|
||||
await page.evaluateOnNewDocument(() => { localStorage.setItem('sp.name', 'Recovery Test'); sessionStorage.setItem('sp.entered', '1'); });
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => globalThis.__SP__?.store.get().connection.status === 'online' && document.querySelector('.lobby-screen'));
|
||||
return page;
|
||||
}
|
||||
test('closed second window resumes the same server player and match through the recovery UI', async () => {
|
||||
const ctx = await browser.createBrowserContext();
|
||||
try {
|
||||
const a = await pageIn(ctx), b = await pageIn(ctx);
|
||||
const before = await b.evaluate(async () => {
|
||||
const { net, store } = globalThis.__SP__;
|
||||
await net.request('room.create', { mode: 'solo', difficulty: 'NORMAL' });
|
||||
await net.request('room.start', {});
|
||||
return { id: store.get().me.playerId, code: store.get().room.code };
|
||||
});
|
||||
await b.waitForFunction(() => globalThis.__SP__.store.get().match.public?.phase === 'INFO_CHECK');
|
||||
await a.waitForSelector('[data-testid="resume-local-match"]');
|
||||
const refused = await a.evaluate(async () => { const { identity } = await import('/js/net.js'); return identity.resume(identity.recoverable()[0].id); });
|
||||
assert.equal(refused, false, 'live window is not displaced');
|
||||
assert.equal(await b.evaluate(() => globalThis.__SP__.store.get().connection.status), 'online');
|
||||
await b.close();
|
||||
await a.click('[data-testid="resume-local-match"]');
|
||||
await a.waitForSelector('.modal .btn--block');
|
||||
await Promise.all([a.waitForNavigation({ waitUntil: 'domcontentloaded' }), a.click('.modal .btn--block')]);
|
||||
await a.waitForFunction(() => globalThis.__SP__?.store.get().match.public?.phase === 'INFO_CHECK');
|
||||
const after = await a.evaluate(() => { const s = globalThis.__SP__.store.get(); return { id: s.me.playerId, code: s.room.code }; });
|
||||
assert.deepEqual(after, before);
|
||||
} finally { await ctx.close(); }
|
||||
});
|
||||
test('co-op opening reroll shows a unanimous vote, clears stale readiness and can be rejected', { timeout: 45000 }, async () => {
|
||||
const ctx = await browser.createBrowserContext();
|
||||
try {
|
||||
const host = await pageIn(ctx), guest = await pageIn(ctx);
|
||||
const code = await host.evaluate(async () => {
|
||||
const { net, store } = globalThis.__SP__;
|
||||
await net.request('room.create', { mode: 'coop', difficulty: 'NORMAL' });
|
||||
return store.get().room.code;
|
||||
});
|
||||
await guest.evaluate(async (code) => {
|
||||
const { net } = globalThis.__SP__;
|
||||
await net.request('room.join', { code }); await net.request('room.ready', { ready: true });
|
||||
}, code);
|
||||
await host.evaluate(() => globalThis.__SP__.net.request('room.start', {}));
|
||||
for (const p of [host, guest]) await p.waitForFunction(() => globalThis.__SP__.store.get().match.public?.phase === 'INFO_CHECK');
|
||||
await guest.evaluate(() => globalThis.__SP__.net.request('g.infoReady', { setupRevision: 0 }));
|
||||
await host.waitForSelector('.brief-reroll button');
|
||||
await host.bringToFront();
|
||||
await host.click('.brief-reroll button');
|
||||
await guest.waitForSelector('.brief-reroll .btn--primary');
|
||||
assert.equal(await guest.evaluate(() => globalThis.__SP__.store.get().match.public.deadline), 0);
|
||||
await guest.bringToFront();
|
||||
await guest.click('.brief-reroll .btn--primary');
|
||||
for (const p of [host, guest]) await p.waitForFunction(() => globalThis.__SP__.store.get().match.public?.setupRevision === 1);
|
||||
const state = await guest.evaluate(() => globalThis.__SP__.store.get().match.public);
|
||||
assert.equal(state.rerollVote, null);
|
||||
assert.ok(state.players.filter((p) => !p.isBot).every((p) => !p.ready));
|
||||
await host.waitForFunction(() => !!document.querySelector('.brief-reroll button:not(:disabled)'));
|
||||
// Wait out the documented server anti-burst interval before a second request.
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
await host.bringToFront();
|
||||
await host.click('.brief-reroll button');
|
||||
await guest.waitForSelector('.brief-reroll .btn--danger');
|
||||
await guest.bringToFront();
|
||||
await guest.click('.brief-reroll .btn--danger');
|
||||
await host.waitForFunction(() => !globalThis.__SP__.store.get().match.public.rerollVote);
|
||||
assert.equal(await host.evaluate(() => globalThis.__SP__.store.get().match.public.setupRevision), 1);
|
||||
} finally { await ctx.close(); }
|
||||
});
|
||||
test('operator voice selection persists across reload and follows global after reset', async () => {
|
||||
const ctx = await browser.createBrowserContext();
|
||||
try {
|
||||
const page = await pageIn(ctx, true);
|
||||
const open = async () => { await page.evaluate(async () => (await import('/js/ui/loadoutSync.js')).openLoadout()); await page.waitForSelector('.lo-card__pick'); await page.click('.lo-card__pick'); await page.waitForSelector('[data-voice-char] select', { visible: true }); };
|
||||
await open();
|
||||
const id = await page.$eval('[data-voice-char]', (e) => e.dataset.voiceChar);
|
||||
await page.select('[data-voice-char] select', 'jp');
|
||||
assert.equal(await page.evaluate(async (id) => (await import('/js/audio.js')).audio.voiceOverrides[id], id), 'jp');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => globalThis.__SP__?.store.get().connection.status === 'online');
|
||||
await open();
|
||||
assert.equal(await page.$eval('[data-voice-char] select', (e) => e.value), 'jp');
|
||||
await page.select('[data-voice-char] select', '');
|
||||
assert.equal(await page.evaluate((id) => JSON.parse(localStorage.getItem('sp.pref.settings')).voiceOverrides[id], id), undefined);
|
||||
} finally { await ctx.close(); }
|
||||
});
|
||||
test('install manifest is eligible and a dismissed offer is consumed through the actual button', async () => {
|
||||
const ctx = browser.defaultBrowserContext();
|
||||
let page;
|
||||
try {
|
||||
page = await pageIn(ctx);
|
||||
const client = await page.createCDPSession();
|
||||
const manifest = await client.send('Page.getAppManifest');
|
||||
assert.deepEqual(manifest.errors, []);
|
||||
const install = await client.send('Page.getInstallabilityErrors');
|
||||
assert.deepEqual(install.installabilityErrors, []);
|
||||
await page.evaluate(() => {
|
||||
const e = new Event('beforeinstallprompt', { cancelable: true });
|
||||
window.promptCalls = 0;
|
||||
e.prompt = async () => { window.promptCalls++; return { outcome: 'dismissed' }; };
|
||||
window.dispatchEvent(e);
|
||||
});
|
||||
await page.waitForSelector('[data-testid="pwa-install"]');
|
||||
await page.click('[data-testid="pwa-install"]');
|
||||
await page.waitForFunction(() => !document.querySelector('[data-testid="pwa-install"]'));
|
||||
assert.equal(await page.evaluate(() => window.promptCalls), 1);
|
||||
} finally { await page?.close(); }
|
||||
});
|
||||
test('extra-large settings stay within a landscape phone in every shipped interface language', async () => {
|
||||
const ctx = await browser.createBrowserContext();
|
||||
try {
|
||||
const page = await pageIn(ctx, true);
|
||||
for (const lang of ['zh-CN', 'en', 'ja', 'ko', 'zh-TW']) {
|
||||
await page.evaluate(async (lang) => {
|
||||
await (await import('/js/ui/lang.js')).switchLang(lang);
|
||||
(await import('/js/ui/settings.js')).updateSettings({ textSize: 'xl' });
|
||||
}, lang);
|
||||
await page.click('[data-testid="settings-btn"]');
|
||||
await page.waitForSelector('.modal .set-list');
|
||||
const overflow = await page.$eval('.modal__body', (e) => e.scrollWidth - e.clientWidth);
|
||||
const excess = await page.$eval('.modal__body', (e) => { const right = e.getBoundingClientRect().right; return [...e.querySelectorAll('*')].filter((x) => x.getBoundingClientRect().right > right + 1).map((x) => [x.className, x.textContent.slice(0, 50), Math.round(x.getBoundingClientRect().width)]); });
|
||||
assert.ok(overflow <= 1, `${lang}: settings overflow ${overflow} ${JSON.stringify(excess)}`);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !document.querySelector('.modal .set-list'));
|
||||
}
|
||||
} finally { await ctx.close(); }
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user