package server import ( "strings" "testing" ) // TestMemberAddonsTruthfulCopy pins the member-product-discovery delta's // Extras bucket (ux-ia-naming D6): an unpurchasable product must never claim // "Coming soon" — that asserts a roadmap that does not exist — nor "Not // available yet", the generic phrasing this bucket retires in favor of the // more precise "not available for purchase". Per the delta spec's // "Purchasability gates the control, not the visibility" scenario, it // renders WITHOUT a purchase control at all (no disabled button), just the // truthful line. A purchasable product renders the normal purchase // affordance (an enabled Add control wired to checkout) with no // unavailability copy at all, and a carried display_category label renders // as a small grouping badge alongside it. func TestMemberAddonsTruthfulCopy(t *testing.T) { t.Run("unpurchasable product shows truthful unavailable copy, no control, not a roadmap claim", func(t *testing.T) { data := AddonsData{Addons: []AddonViewModel{ {ProductID: "p1", Name: "Extra Storage", Description: "More room.", Purchasable: false}, }} out := renderMemberDomains(t, "member_addons.html", data) if strings.Contains(out, "Coming soon") { t.Error("unpurchasable product must not claim \"Coming soon\" (fictional roadmap claim)") } if !strings.Contains(out, "Not available for purchase") { t.Error("unpurchasable product must state truthfully that it is not available for purchase") } if strings.Contains(out, "addon`) { t.Error("a carried display_category must render as a small grouping badge") } }) t.Run("blank display_category renders no badge", func(t *testing.T) { data := AddonsData{Addons: []AddonViewModel{ {ProductID: "p3", Name: "Extra Storage", Description: "More room.", PriceID: "price-1", Purchasable: true}, }} out := renderMemberDomains(t, "member_addons.html", data) if strings.Contains(out, `class="badge`) { t.Error("a blank display_category must not render a grouping badge") } }) } // TestMemberAddonsHeadingAndDefinition pins the non-plan section's // conditional heading (design.md D6, maintainer decision 2026-08-23): // "More products" beneath plan sections (with the definition line), plain // "Products" when no plans exist and this section is the whole catalog — // a plan-less deployment must not read its catalog as extras. "Add-ons" // stays retired: a solo-purchasable product is not an addition to anything. func TestMemberAddonsHeadingAndDefinition(t *testing.T) { row := []AddonViewModel{ {ProductID: "p1", Name: "Extra Storage", Description: "More room.", PriceID: "price-1", Purchasable: true}, } withPlans := renderMemberDomains(t, "member_addons.html", AddonsData{Addons: row, HasPlans: true}) if !strings.Contains(withPlans, ">More products<") { t.Error(`with plans above, expected the section heading "More products"`) } if !strings.Contains(withPlans, "Products you can add alongside your plan, or use on their own.") { t.Error("with plans above, expected the definition line") } alone := renderMemberDomains(t, "member_addons.html", AddonsData{Addons: row, HasPlans: false}) if !strings.Contains(alone, ">Products<") { t.Error(`standing alone, expected the plain heading "Products"`) } if strings.Contains(alone, "alongside your plan") { t.Error("standing alone, the plan-referencing definition line must not render") } for _, out := range []string{withPlans, alone} { if strings.Contains(out, ">Add-ons<") || strings.Contains(out, ">Extras<") { t.Error(`"Add-ons" and "Extras" must both be retired as the bucket heading`) } } } // TestMemberAddonsNoQualifyingProductsRendersNothing pins the "No qualifying // products" scenario: an empty Addons slice renders no section at all — no // heading, no table, no empty-state chrome. func TestMemberAddonsNoQualifyingProductsRendersNothing(t *testing.T) { out := renderMemberDomains(t, "member_addons.html", AddonsData{}) if strings.TrimSpace(strings.ReplaceAll(out, "\n", "")) == "" { return // an all-comment render collapses to whitespace; that's fine } for _, unwanted := range []string{">More products<", ">Products<", "