Files
member-console/internal/server/member_addons_test.go
T
cgalo5758 9b96e9c9e9 Rework operator IA and unify UI vocabulary
- Restructure operator sidebar into a flat task list with indented
  children; fold plan topology into plan ladders
- Expand member catalog non-plan section to all published non-tier
  products; require recurring Stripe-mapped prices for purchase
- Add operator domains placements and terminal-claims ledger; redirect
  /domains to the FedWiki Sites Domains anchor
- Apply canonical vocabulary and chrome/form conventions; migrate seeded
  FedWiki Sites display name
2026-08-23 17:12:42 -05:00

119 lines
5.4 KiB
Go

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, "<button") {
t.Error("unpurchasable product must render without a purchase control at all, not a disabled button")
}
if strings.Contains(out, `hx-post="/billing/checkout"`) {
t.Error("unpurchasable product must not submit to checkout")
}
})
t.Run("purchasable product renders the normal purchase affordance and a category badge, no unavailability copy", func(t *testing.T) {
data := AddonsData{Addons: []AddonViewModel{
{ProductID: "p2", Name: "Extra Storage", Description: "More room.", DisplayCategory: "addon", PriceID: "price-1", Purchasable: true},
}}
out := renderMemberDomains(t, "member_addons.html", data)
if strings.Contains(out, "Not available for purchase") || strings.Contains(out, "Coming soon") {
t.Error("purchasable product must not render unavailability copy")
}
if strings.Contains(out, "disabled") {
t.Error("purchasable product's control must not render disabled")
}
if !strings.Contains(out, `hx-post="/billing/checkout"`) || !strings.Contains(out, `"price_id": "price-1"`) {
t.Error("purchasable product must submit to checkout with its price id")
}
if !strings.Contains(out, `<span class="badge bg-secondary">addon</span>`) {
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<", "<table", "<h2", "<h6"} {
if strings.Contains(out, unwanted) {
t.Errorf("no qualifying products must render no section chrome; found %q in: %s", unwanted, out)
}
}
}