feedback1 WB: bounty-half evidence stated as a trend with its counter-evidence; queued free offers counted on the pill and header

- tools/build-data.mjs bountyDraftHalf: exact faction-series counts (12 of 18 two-battle cards bring first-half
  enemies, 5 outside the wave tables, enemyeffect_12_4 a second-half attachment), base-HP gradient, the user's
  two-battle recall; counter-evidence (假想敌 named enemyInitial_*, the unread NGA R3 guide); the split marked
  [ASSUMED] with its flip; data unchanged
- server/match/choices.js, docs/META.md, docs/DATA.md: the split marked [ASSUMED]
- m.private shop.rewardOffer.queued: offers waiting behind the shown one; the header says 之后还有 N 项 and the
  reminder pill +N (a 定向投放 behind a promotion reward put off)
- tests: corrected headers, the stale bountyTiers fixture dropped, queued checks (unit + opt-in e2e on desktop
  and 756x366)
This commit is contained in:
sganggs committed 2026-10-03 02:38:42 +08:00
1 parent 49c0ebcf4d
commit 6e0c26af9e
14 files changed
+158 -37

No files matched your search

+61 -1
View File
@@ -4,7 +4,9 @@
// 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.
// errors. Screenshots: test/e2e/out/fb1-offer-*.png. A second case puts the offer off (稍后) and levels up again: the
// pill reads 定向投放待选择 +1, the header 之后还有 1 项, and the second offer follows the first pick
// (test/e2e/out/fb1-queue-*.png).
//
// SP_E2E=1 node --test test/ui/feedback1-offers.e2e.test.js
@@ -139,4 +141,62 @@ describe('player report #6: 凯瑟琳\'s three free items after a level-up (real
await srv.stop();
}
});
test('an offer put off (稍后) and a second level-up: the pill says "+1", the header "之后还有 1 项", then the second offer', { timeout: 180000 }, 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, 'cathy2', { prefix: 'fb1-queue' });
const levelUp = async () => { await c.click('.lvcard'); await sleep(250); await c.click('.lvcard'); };
const offerOf = () => c.page.evaluate(() => globalThis.__SP__.store.get().match.private?.shop?.rewardOffer || null);
try {
await soloToPrep(c, '凯瑟琳');
await levelUp();
await c.page.waitForFunction(() => !!document.querySelector('.shopbar__reward .scard'), { timeout: 8000 });
await c.click('.rwtag__later');
await c.page.waitForFunction(() => !!document.querySelector('.rewardpill') && !!document.querySelector('.shopbar__cards'), { timeout: 8000 });
await levelUp();
await c.page.waitForFunction(() => globalThis.__SP__.store.get().match.private?.shop?.rewardOffer?.queued === 1, { timeout: 8000 });
await sleep(400);
const pill = await c.page.evaluate(() => document.querySelector('.rewardpill')?.textContent.replace(/\s+/g, '') || '');
assert.match(pill, /定向投放待选择/);
assert.match(pill, /\+1/, `the pill counts the offer behind (${pill})`);
await c.shot('pill');
await c.click('.rewardpill');
await c.page.waitForFunction(() => !!document.querySelector('.shopbar__reward .scard'), { timeout: 8000 });
await sleep(300);
assert.equal(await c.page.evaluate(() => document.querySelector('.rwtag__more')?.textContent.trim() || null), '之后还有 1 项');
await c.shot('offer');
// the narrowest phone: the header strip still holds every line and the 稍后 button inside the screen
const browserUa = await c.browser.userAgent();
await c.page.emulate({ viewport: { width: 756, height: 366, deviceScaleFactor: 2, isMobile: true, hasTouch: true, isLandscape: true }, userAgent: browserUa });
await c.page.waitForFunction(() => !!document.querySelector('.rwtag__more'), { timeout: 30000 });
await sleep(900);
await c.page.screenshot({ path: path.join(ROOT, 'test/e2e/out', 'fb1-queue-756x366.png') });
const strip = await c.page.evaluate(() => {
const box = (el) => { const r = el.getBoundingClientRect(); return { l: r.left, t: r.top, r: r.right, b: r.bottom }; };
const tag = document.querySelector('.rwtag');
return { tag: box(tag), parts: [...tag.children].map((el) => ({ cls: el.className, ...box(el) })), vw: innerWidth, vh: innerHeight };
});
for (const part of strip.parts) {
assert.ok(part.t >= strip.tag.t - 0.5 && part.b <= strip.tag.b + 0.5 && part.b <= strip.vh + 0.5, `756×366 ${part.cls}: inside the strip and the screen ${JSON.stringify(strip)}`);
}
await c.page.emulate({ viewport: { width: 1920, height: 1080, deviceScaleFactor: 1, isMobile: false, hasTouch: false }, userAgent: browserUa });
await c.page.waitForFunction(() => !!document.querySelector('.shopbar__reward .scard'), { timeout: 30000 });
await sleep(900);
await c.hookRequests();
const first = await offerOf();
await c.click('.shopbar__reward .scard');
await sleep(300);
await c.click('.shopbar__reward .scard.is-armed');
await c.page.waitForFunction((ids) => { const o = globalThis.__SP__.store.get().match.private?.shop?.rewardOffer; return !!o && o.queued === 0 && o.slots.map((x) => x.id).join() !== ids; }, { timeout: 8000 }, first.slots.map((x) => x.id).join());
await sleep(300);
assert.equal(await c.exists('.rwtag__more'), false, 'nothing behind the second offer');
assert.equal(await c.page.evaluate(() => document.querySelector('.rwtag__title')?.textContent.trim()), '定向投放');
await c.shot('second');
assert.deepEqual(problemsOf([c]), []);
} finally {
await c.close();
await srv.stop();
}
});
});