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:
1 parent
78251f499c
commit
0e8004a382
32 files changed
+518
-181
No files matched your search
+45
-24
@@ -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'));
|
||||
|
||||
Reference in new issue
Block a user