- 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
119 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|