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); });