From ab8bbeae5d34923aba9652359eaf069820a16a0f Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sat, 22 Aug 2026 21:06:46 +0200 Subject: [PATCH] fix(shop): render a bag offer's capacity line once, not twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- apps/api/src/shops/shop.service.ts | 8 +++++--- .../features/npc/merchant-page.component.html | 8 +++++--- .../npc/merchant-page.component.spec.ts | 19 +++++++++++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/api/src/shops/shop.service.ts b/apps/api/src/shops/shop.service.ts index 81f40e5..19e5a79 100644 --- a/apps/api/src/shops/shop.service.ts +++ b/apps/api/src/shops/shop.service.ts @@ -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), }; diff --git a/apps/web/src/app/features/npc/merchant-page.component.html b/apps/web/src/app/features/npc/merchant-page.component.html index cad6e7a..3841a9c 100644 --- a/apps/web/src/app/features/npc/merchant-page.component.html +++ b/apps/web/src/app/features/npc/merchant-page.component.html @@ -234,9 +234,11 @@ />
{{ offer.itemName }} - {{ - offer.itemDescription - }} + @if (offer.itemDescription) { + {{ + offer.itemDescription + }} + } @if (offer.effectSummary) { {{ offer.effectSummary diff --git a/apps/web/src/app/features/npc/merchant-page.component.spec.ts b/apps/web/src/app/features/npc/merchant-page.component.spec.ts index d3aeaef..a8cc064 100644 --- a/apps/web/src/app/features/npc/merchant-page.component.spec.ts +++ b/apps/web/src/app/features/npc/merchant-page.component.spec.ts @@ -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); });