feedback5 WN: 补位 display follows the official game (the owner's recall of 2026-10-06) — a chess the player does not own is shown as its stand-in wherever the player's copy appears: shop and reward cards (the stand-in's portrait, name, class and backup skill; the chess's bonds, tier and price; a small 「替补」 mark), the hand and 临时整备区 pieces (the stand-in's model, a 「替补」 tag; the board already was), the detail card (the stand-in's portrait, name, skill, talents and module with the chess's bonds and 特质, 「银灰的替补」 under the name [ASSUMED]), bond popups (own and a teammate's by UnitInfo standInFor through ownerBoard / ownerOps), the result lineup (m.result standInFor), prep scouting of the bench, the equip-replace dialog, the elite and gift tickers; rules unchanged (bonds, price, tier, merges, pools and 特质 stay the chess's); new strings through t() with English; tests updated (standin-ui 15, standin-ownership 7, watch-bonds wiring) and the e2e now ends on the result screen (fastServer SP_FINISH_AFTER); golden unchanged

This commit is contained in:
sganggs committed 2026-10-06 02:40:38 +08:00
1 parent 78251f499c
commit 0e8004a382
32 files changed
+518 -181

No files matched your search

+45 -24
View File
@@ -1,15 +1,18 @@
// Real-server browser E2E of 0.2.0 补位 end to end (the approved plan, owner's decision 2026-10-05): the 干员持有 tab of
// the 干员调配 overlay marks 银灰 as not owned (real clicks; persisted in localStorage; room.ownership → session → seat →
// the next match's m.private.standIns); in the match the shop card keeps 银灰 (name, price) with 「替补:Sharp」; bought,
// the hand piece carries the tag; deployed, the board model is Sharp's; the detail card shows Sharp's skill under
// 银灰's name; and in the browser's own battle (client-side combat) the unit is the stand-in — the b.start spec marks it
// `standIn: true`, the local sim fields char_609_acguad with its kit, and the battle view draws Sharp's Spine model.
// Real-server browser E2E of 0.2.0 补位 end to end (the approved plan, owner's decision 2026-10-05; the display follows
// the owner's recall of the official mode, 2026-10-06): the 干员持有 tab of the 干员调配 overlay marks 银灰 as not owned
// (real clicks; persisted in localStorage; room.ownership → session → seat → the next match's m.private.standIns); in
// the match the shop card shows the stand-in Sharp (name, portrait, a small 「替补」 mark; 银灰's bonds and price);
// bought, the hand piece is Sharp's model with the 「替补」 tag; deployed, the board model is Sharp's and the detail
// card shows Sharp (name, skill) with 「银灰的替补」; in the browser's own battle (client-side combat) the unit is the
// stand-in — the b.start spec marks it `standIn: true`, the local sim fields char_609_acguad with its kit, and the
// battle view draws Sharp's Spine model; the match then ends (the fast server's finishAfter hook) and the result
// screen's lineup shows Sharp with the 「替补」 mark.
//
// SP_E2E=1 node --test test/ui/standin.e2e.test.js
//
// The server is test/e2e/fastServer.mjs with its starter-kit hooks (no starter operators, 银灰 in the first shop slot,
// 20 funds); everything else is the real match engine and the real UI driven by real mouse input.
// Screenshots: test/e2e/out/standin-*.png.
// 20 funds) and SP_FINISH_AFTER 1 (the match ends once round 1 has settled); everything else is the real match engine
// and the real UI driven by real mouse input. Screenshots: test/e2e/out/standin-*.png.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
@@ -21,8 +24,8 @@ const SILVER = 'chess_char_4_22_a'; // 银灰 (NORMAL) → Sharp (char_609_acgua
const SHARP = 'char_609_acguad';
describe('0.2.0 补位 — a not-owned operator fights as its stand-in (real server)', { skip: !ENABLED && 'set SP_E2E=1 (Chrome + public/assets)' }, () => {
test('干员持有: drop 银灰 → buy it (badge) → deploy (Sharp model, Sharp card) → the local battle fields Sharp', { timeout: 6 * 60 * 1000 }, async () => {
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 2, startRound: 1, kit: 0, shop: [SILVER] } });
test('干员持有: drop 银灰 → the shop shows Sharp → buy (the hand shows Sharp) → deploy (Sharp card) → the local battle fields Sharp → the result shows Sharp', { timeout: 6 * 60 * 1000 }, async () => {
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 2, startRound: 1, kit: 0, shop: [SILVER], finishAfter: 1 } });
const P = (await import('puppeteer-core')).default;
const c = new Client(P, srv.base, 'standin', { prefix: 'standin' });
try {
@@ -62,28 +65,34 @@ describe('0.2.0 补位 — a not-owned operator fights as its stand-in (real ser
await c.waitFor((s) => s.phase === 'PREP' && !s.ready && s.funds >= 3, 'prep with funds', 60000);
await sleep(1800); // camera flight
// 3) the shop card: 银灰 with 「替补:Sharp」; two taps buy it
// 3) the shop card shows the stand-in: Sharp (name, portrait) with a small 「替补」 mark, 银灰's bond; two taps buy it
await c.page.waitForSelector('.scard .scard__standin', { visible: true, timeout: 10000 });
const shopCard = await c.page.evaluate(() => {
const el = document.querySelector('.scard .scard__standin')?.closest('.scard');
return el ? { name: el.querySelector('.scard__name')?.textContent, badge: el.querySelector('.scard__standin')?.textContent, standIn: el.querySelector('.scard__standin')?.dataset.standin } : null;
return el ? {
name: el.querySelector('.scard__name')?.textContent, badge: el.querySelector('.scard__standin')?.textContent,
standIn: el.querySelector('.scard__standin')?.dataset.standin, for: el.querySelector('.scard__standin')?.dataset.for,
art: el.querySelector('.scard__art')?.getAttribute('src') || '', bonds: [...el.querySelectorAll('.scard__bond > span:last-child')].map((b) => b.textContent),
} : null;
});
assert.deepEqual(shopCard, { name: '银灰', badge: '替补:Sharp', standIn: SHARP });
assert.ok(shopCard, 'the stand-in card');
assert.deepEqual([shopCard.name, shopCard.badge, shopCard.standIn, shopCard.for], ['Sharp', '替补', SHARP, SILVER]);
assert.match(shopCard.art, /char_609_acguad/, 'Sharp\'s portrait');
assert.deepEqual(shopCard.bonds, ['谢拉格'], '银灰\'s bond still counts');
await c.shot('shop');
await c.click('.scard', '银灰');
await c.click('.scard', 'Sharp');
await sleep(300);
await c.click('.scard', '银灰');
await c.click('.scard', 'Sharp');
await c.waitFor((s) => s.hand > 0, 'bought', 8000);
await sleep(900);
const [piece] = (await c.handPieces('chess')).filter((p) => p.id === SILVER);
assert.ok(piece, '银灰 is in the hand');
// the hand piece keeps 银灰's model, tagged 「替补:Sharp」
await c.page.waitForFunction((uid) => document.querySelector(`.sitag[data-uid="${uid}"]`)?.textContent === '替补:Sharp', { timeout: 5000 }, piece.uid);
const handModel = await c.page.evaluate((uid) => globalThis.__SP_VIEW__?.raw?.debug?.views?.get(`p:${uid}`)?.info?.spine ?? null, piece.uid);
assert.notEqual(handModel, SHARP, 'the hand shows the original operator');
assert.ok(piece, 'the 银灰 piece is in the hand');
// the hand piece is Sharp's model, tagged 「替补」
await c.page.waitForFunction((uid) => document.querySelector(`.sitag[data-uid="${uid}"]`)?.textContent === '替补', { timeout: 5000 }, piece.uid);
await c.page.waitForFunction((uid, sharp) => globalThis.__SP_VIEW__?.raw?.debug?.views?.get(`p:${uid}`)?.info?.spine === sharp, { timeout: 5000 }, piece.uid, SHARP);
await c.shot('hand');
// 4) deploy it (drag → direction wheel): the board model becomes Sharp's; the card shows Sharp's body
// 4) deploy it (drag → direction wheel): the board model stays Sharp's; the card shows Sharp, 「银灰的替补」
const tile = await c.freeTileFor(piece.uid);
assert.ok(tile, 'a legal tile');
await c.drag(await c.piecePoint(piece.uid), await c.tilePoint(tile.row, tile.col));
@@ -103,12 +112,13 @@ describe('0.2.0 补位 — a not-owned operator fights as its stand-in (real ser
detail = await c.page.waitForSelector('.dpanel .dtag-standin', { timeout: 2500 }).then(() => c.page.evaluate(() => ({
tag: document.querySelector('.dpanel .dtag-standin')?.textContent || '',
name: document.querySelector('.dpanel .dhead__name')?.textContent || '',
for: document.querySelector('.dpanel .dhead__for')?.textContent || '',
skill: document.querySelector('.dpanel .dskill')?.dataset.skill || '',
})), () => null);
if (detail) break;
}
assert.ok(detail, 'the detail card opens for the deployed piece');
assert.deepEqual(detail, { tag: '替补:Sharp', name: '银灰', skill: 'skchr_acguad_2' });
assert.deepEqual(detail, { tag: '替补', name: 'Sharp', for: '银灰的替补', skill: 'skchr_acguad_2' });
await c.shot('board');
await c.page.keyboard.press('Escape');
@@ -140,8 +150,19 @@ describe('0.2.0 补位 — a not-owned operator fights as its stand-in (real ser
assert.match(got.model, /char_609_acguad/, 'the battle view draws Sharp\'s Spine model');
await sleep(800);
await c.shot('combat');
// the battle ends and the round settles normally (the result upload is accepted)
await c.waitFor((s) => (s.phase === 'PREP' && s.round >= 2) || !!s.result, 'next prep', 180000);
// 6) the battle ends, the round settles (the result upload is accepted) and the match ends (finishAfter 1): the
// result screen's lineup shows Sharp with the 「替补」 mark
await c.waitFor((s) => !!s.result, 'result', 180000);
await c.page.waitForSelector('.rcard.is-self .rcard__lineup .uthumb', { visible: true, timeout: 15000 });
const lineup = await c.page.evaluate(() => [...document.querySelectorAll('.rcard.is-self .rcard__lineup .uthumb')].map((el) => ({
title: el.getAttribute('title') || '', mark: el.querySelector('.uthumb__si')?.textContent || '',
standIn: el.querySelector('.uthumb__si')?.dataset.standin || '', art: el.querySelector('img')?.getAttribute('src') || '',
})));
assert.equal(lineup.length, 1, 'the one operator fielded');
assert.deepEqual([lineup[0].title, lineup[0].mark, lineup[0].standIn], ['Sharp(银灰的替补)', '替补', SHARP]);
assert.match(lineup[0].art, /char_609_acguad/, 'Sharp\'s avatar');
await sleep(400);
await c.shot('result');
assert.deepEqual(problemsOf([c]), []);
} finally {
if (c.problems.length) console.log(c.problems.slice(0, 20).join('\n'));