Files
ark-stronghold-protocol/test/ui/feedback1-offers.e2e.test.js
T
sganggs 49c0ebcf4d feedback1 WB: special-refresh offers drawn as what they hold (凯瑟琳's three items as item cards)
Player report #6: 凯瑟琳's 定向投放 offer after a level-up showed three nameless
operator cards (the raw item ids) under the promotion reward's 晋升奖励 header.
The shop bar now draws each offer slot by its kind (item slots as free item
cards) and names the offer: m.private shop.rewardOffer carries source
('merge' | 'special') and label (the strategy's effect name, the item's name
or the 特质's operator — effectsMeta.offerLabel); the reminder pill says
<label>待选择. Same path for 娜仁图亚, 寻呼模块, 信标 and 松果.
2026-10-03 01:48:29 +08:00

143 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Real-server browser E2E of player report #6 after 0.1.0: "凯瑟琳策略升级刷新的三个装备显示异常". A solo 险境 prep with the
// strategy 凯瑟琳 (【定向投放】"每次升级调度中心时,在调度中心刷新随机3件装备,可以选择并获得其中1件"; test/e2e/fastServer.mjs
// SP_START_ROUND 2: a starter kit + 20 funds) is driven with real input: the LEVEL card is tapped twice (确认升级) and the
// three free items replace the bar's operator cards — each drawn as an item card (icon, name, tier, description, FREE),
// under a 定向投放 header (not 晋升奖励 / PROMOTION), with the item's detail on the first tap; the second tap takes it into
// the hand and the bar shows the shop again. Desktop 1920×1080 and the phones 844×390 / 756×366 (touch). Zero console
// errors. Screenshots: test/e2e/out/fb1-offer-*.png.
//
// SP_E2E=1 node --test test/ui/feedback1-offers.e2e.test.js
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { Client, ROOT, sleep, hasChrome, startRealServer, problemsOf } from '../e2e/client.mjs';
const ENABLED = process.env.SP_E2E === '1' && hasChrome() && existsSync(path.join(ROOT, 'public/assets'));
async function soloToPrep(c, bandName) {
await c.open();
await c.enter('煌');
await c.click('.mode-card', '独立模拟');
await c.click('.diff-card', '险境');
await c.click('.create-box button', '开始独立模拟');
await c.waitFor((s) => !!s.room, 'solo room');
if (!(await c.st()).phase) await c.click('.room-bar__right button', '开始模拟', { timeout: 20000 });
await c.waitFor((s) => s.phase === 'INFO_CHECK', 'briefing', 30000);
await c.click('.brief__foot .btn--primary', '准备就绪');
await c.waitFor((s) => s.phase === 'BAND_DRAFT', 'band draft', 30000);
await c.click('.dband', bandName);
await c.click('.draft-detail__btns .btn--primary', '确认选择');
await c.waitFor((x) => x.phase === 'PREP' && !x.ready, 'prep', 60000);
await sleep(1800);
}
/** The offer as drawn: header texts, the cards (kind, name, description, art, free tag) and their boxes. */
function offerDom(c) {
return c.page.evaluate(() => {
const box = (el) => { if (!el) return null; const r = el.getBoundingClientRect(); return { l: r.left, t: r.top, r: r.right, b: r.bottom, w: r.width, h: r.height }; };
const root = document.querySelector('.shopbar__reward');
if (!root) return null;
const cards = [...root.querySelectorAll('.scard')].map((el) => ({
item: el.classList.contains('scard--item'),
name: el.querySelector('.scard__name')?.textContent.trim() || '',
desc: el.querySelector('.scard__idesc')?.textContent.trim() || '',
icon: !!el.querySelector('.scard__itemart img'),
free: !!el.querySelector('.scard__free'),
tier: el.querySelector('.tierchip, [class*="tier"]')?.textContent.trim() || '',
box: box(el),
clipped: el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1,
}));
return {
title: root.querySelector('.rwtag__title')?.textContent.trim() || '',
micro: root.querySelector('.rwtag__micro')?.textContent.trim() || '',
sub: root.querySelector('.rwtag__sub')?.textContent.trim() || '',
cards, root: box(root), vw: innerWidth, vh: innerHeight,
};
});
}
const privOf = (c) => c.page.evaluate(() => globalThis.__SP__.store.get().match.private);
describe('player report #6: 凯瑟琳\'s three free items after a level-up (real server, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome + assets) to run' }, () => {
test('the level-up offer shows three item cards (desktop and phones); a pick puts the item into the hand', { timeout: 240000 }, async () => {
const puppeteer = (await import('puppeteer-core')).default;
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 4, startRound: 2 } });
const c = new Client(puppeteer, srv.base, 'cathy', { prefix: 'fb1-offer' });
try {
await soloToPrep(c, '凯瑟琳');
assert.equal((await privOf(c)).bandId, 'band_cathy', 'strategy 凯瑟琳');
// LEVEL: first tap arms 确认升级, the second upgrades
await c.click('.lvcard');
await sleep(250);
await c.click('.lvcard');
await c.page.waitForFunction(() => !!globalThis.__SP__.store.get().match.private?.shop?.rewardOffer, { timeout: 8000 });
await sleep(600);
const priv = await privOf(c);
const offer = priv.shop.rewardOffer;
assert.equal(offer.slots.length, 3, 'three items');
assert.ok(offer.slots.every((s) => s.kind === 'item' && s.price === 0), JSON.stringify(offer));
const names = await c.page.evaluate((ids) => ids.map((id) => globalThis.__SP__.data.lookup('items', id)?.name), offer.slots.map((s) => s.id));
await c.shot('desktop');
const dom = await offerDom(c);
assert.ok(dom, 'the offer is in the bar');
assert.equal(dom.title, '定向投放', `the header names the strategy (${dom.title} / ${dom.micro})`);
assert.match(dom.sub, /免费选择 1 件/);
assert.deepEqual(dom.cards.map((x) => x.name), names, 'the item names');
for (const card of dom.cards) {
assert.ok(card.item, `${card.name}: drawn as an item card`);
assert.ok(card.icon, `${card.name}: the item icon`);
assert.ok(card.desc.length > 2, `${card.name}: the description`);
assert.ok(card.free, `${card.name}: FREE`);
}
// phones (touch, landscape): the same cards inside the viewport, none overlapping another
const browserUa = await c.browser.userAgent();
for (const [w, h] of [[844, 390], [756, 366]]) {
await c.page.emulate({ viewport: { width: w, height: h, deviceScaleFactor: 2, isMobile: true, hasTouch: true, isLandscape: true }, userAgent: browserUa });
await c.page.waitForFunction(() => !!document.querySelector('.shopbar__reward .scard'), { timeout: 30000 });
await sleep(900);
await c.page.screenshot({ path: path.join(ROOT, 'test/e2e/out', `fb1-offer-${w}x${h}.png`) });
const d = await offerDom(c);
assert.ok(d && d.cards.length === 3, `${w}×${h}: three cards`);
for (const card of d.cards) {
assert.ok(card.item && card.icon, `${w}×${h} ${card.name}: an item card with its icon`);
assert.ok(card.box.l >= 0 && card.box.r <= w + 0.5 && card.box.t >= 0 && card.box.b <= h + 0.5, `${w}×${h} ${card.name}: inside the viewport ${JSON.stringify(card.box)}`);
}
for (let i = 1; i < d.cards.length; i++) assert.ok(d.cards[i].box.l >= d.cards[i - 1].box.r - 0.5, `${w}×${h}: cards ${i - 1} / ${i} do not overlap`);
}
await c.page.emulate({ viewport: { width: 1920, height: 1080, deviceScaleFactor: 1, isMobile: false, hasTouch: false }, userAgent: browserUa });
// switching touch emulation reloads the page: the session resumes with the offer still queued (re-hook the intents)
await c.page.waitForFunction(() => !!globalThis.__SP__?.store.get().match.private?.shop?.rewardOffer && !!document.querySelector('.shopbar__reward'), { timeout: 30000 });
await sleep(900);
await c.hookRequests();
// first tap: armed + the item's detail; second tap: taken
const want = names[1];
await c.click('.shopbar__reward .scard', want);
await sleep(400);
const armed = await c.page.evaluate(() => ({
armed: document.querySelector('.shopbar__reward .scard.is-armed .scard__name')?.textContent.trim() || null,
strip: document.querySelector('.shopbar__reward .scard.is-armed .scard__confirm')?.textContent.trim() || null,
detail: document.querySelector('.dpanel')?.textContent || '',
}));
assert.equal(armed.armed, want, 'the tapped card is armed');
assert.match(armed.strip || '', /确认选择/);
assert.ok(armed.detail.includes(want), 'the detail card shows the item');
await c.shot('armed');
await c.click('.shopbar__reward .scard.is-armed');
await c.page.waitForFunction(() => !globalThis.__SP__.store.get().match.private?.shop?.rewardOffer, { timeout: 8000 });
const after = await privOf(c);
const got = await c.page.evaluate((hand) => hand.filter((p) => p && p.kind === 'item').map((p) => globalThis.__SP__.data.lookup('items', p.id)?.name), after.hand);
assert.ok(got.includes(want), `the item is in the hand (${got.join(', ')})`);
assert.equal((await c.requests('g.reward')).length, 1, 'one g.reward');
await sleep(300);
assert.ok(await c.exists('.shopbar__cards'), 'the shop is back');
await c.shot('after');
assert.deepEqual(problemsOf([c]), []);
} finally {
await c.close();
await srv.stop();
}
});
});