fix(shop): render a bag offer's capacity line once, not twice

`resolveTarget` gave a bag target the same string for `description` and
`effectSummary`, and the shop row renders both, so the slice's two flagship
offers showed "Capacity: 5 Raider Trophies" on consecutive lines. Spec §5's
worked example shows it once.

A bag definition carries no flavour text of its own, so the description is now
empty and the row omits the span entirely rather than emitting an empty one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-22 21:06:46 +02:00
parent 81e44b2c15
commit ab8bbeae5d
3 changed files with 27 additions and 8 deletions

View File

@@ -307,9 +307,11 @@ export class ShopService {
definitionId: offer.lootBagDefinition.id,
key: offer.lootBagDefinition.key,
name: offer.lootBagDefinition.name,
// A bag definition carries no flavour text of its own; the capacity
// line is the honest description of what it is.
description: describeBagEffect(offer.lootBagDefinition),
// A bag definition carries no flavour text of its own. The capacity
// line is the whole of what there is to say about it, and it is already
// carried by `effectSummary`; repeating it here would render it twice
// (slice §5 shows the line once).
description: '',
iconPath: offer.lootBagDefinition.iconPath,
effectSummary: describeBagEffect(offer.lootBagDefinition),
};

View File

@@ -234,9 +234,11 @@
/>
<div class="shop-row__naming">
<span class="shop-row__name">{{ offer.itemName }}</span>
<span class="shop-row__description">{{
offer.itemDescription
}}</span>
@if (offer.itemDescription) {
<span class="shop-row__description">{{
offer.itemDescription
}}</span>
}
@if (offer.effectSummary) {
<span class="shop-row__effect" data-effect>{{
offer.effectSummary

View File

@@ -232,6 +232,11 @@ async function renderMerchantWithShop(
return { fixture, element, store };
}
/** How many times `needle` shows up in `haystack` -- `toContain` cannot say. */
function countOccurrences(haystack: string, needle: string): number {
return haystack.split(needle).length - 1;
}
describe('MerchantPageComponent', () => {
afterEach(() => TestBed.resetTestingModule());
@@ -383,13 +388,15 @@ describe('MerchantPageComponent', () => {
it('shows the requirement and the current value on a locked offer', async () => {
// A visible lock with a number attached is a goal; a hidden offer is not
// (slice §5).
// (slice §5). Shaped like the real Trophy Pouch offer: a bag definition
// carries no flavour text, so the API sends an empty description and the
// capacity line arrives once, as the effect.
const { fixture } = await renderMerchantWithShop({
offers: [
{
itemKey: 'basic-trophy-pouch',
itemName: 'Basic Trophy Pouch',
itemDescription: 'Capacity: 5 Raider Trophies',
itemDescription: '',
iconPath: '/images/items/basic-trophy-pouch.png',
currencyType: 'SILVER',
price: 40,
@@ -414,6 +421,14 @@ describe('MerchantPageComponent', () => {
);
expect(row.textContent).toContain('Requires Border Watch Reputation 25');
expect(row.textContent).toContain('Current: 14');
// Once, not twice: slice §5's worked example shows the capacity line a
// single time.
expect(
countOccurrences(row.textContent, 'Capacity: 5 Raider Trophies'),
).toBe(1);
// An empty description renders nothing at all, rather than an empty span
// that would let a duplicated capacity line back in unnoticed.
expect(row.querySelector('.shop-row__description')).toBeNull();
expect(row.querySelector('button').disabled).toBe(true);
});