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