fix(web): render plan names + price in subscription feature matrix headers
The comparison table's column headers were empty <th/> cells (aria-label only), so sighted users could not tell which column belonged to which plan. Headers now show plan name + monthly price + the Popüler badge. Added a "Marka sayısı" row exposing the brand limit (1/2/3/Tümü) — the attribute that actually differentiates the brand1/brand2/brand3 tiers, which the matrix previously omitted entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,8 @@
|
||||
"billingPeriod": "Billing Period",
|
||||
"planComparison": "Plan Comparison",
|
||||
"featureMatrix": "Feature Comparison",
|
||||
"matrixBrandCount": "Brand count",
|
||||
"brandCountAll": "All",
|
||||
"selectBrands": "Select Brands",
|
||||
"selectBrandsDescription": "Choose the brands you want to include in your plan.",
|
||||
"brandsSelected": "brands selected",
|
||||
|
||||
@@ -171,6 +171,8 @@
|
||||
"billingPeriod": "Fatura Dönemi",
|
||||
"planComparison": "Plan Karşılaştırması",
|
||||
"featureMatrix": "Özellik Karşılaştırması",
|
||||
"matrixBrandCount": "Marka sayısı",
|
||||
"brandCountAll": "Tümü",
|
||||
"selectBrands": "Marka Seçin",
|
||||
"selectBrandsDescription": "Planınıza dahil etmek istediğiniz markaları seçin.",
|
||||
"brandsSelected": "marka seçildi",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Regression tests for the FeatureMatrix comparison table.
|
||||
*
|
||||
* Guards the FN fix where the table's column headers rendered EMPTY (`<th />`
|
||||
* with only an aria-label), making it impossible for sighted users to tell
|
||||
* which column belonged to which plan. The headers must now render the plan
|
||||
* name + monthly price, and a "brand count" row must expose the one attribute
|
||||
* that actually differentiates the brand1/brand2/brand3 tiers.
|
||||
*/
|
||||
|
||||
import { render, within } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/i18n", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
locale: "tr",
|
||||
setLocale: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { FeatureMatrix, plans } from "@/routes/dashboard/subscription/index";
|
||||
|
||||
describe("FeatureMatrix column headers (CRO fix)", () => {
|
||||
test("every plan name is rendered as a visible column header", () => {
|
||||
const { container } = render(<FeatureMatrix />);
|
||||
const headerCells = Array.from(container.querySelectorAll("thead th"));
|
||||
// 1 empty label column + one per plan
|
||||
expect(headerCells).toHaveLength(plans.length + 1);
|
||||
|
||||
for (const plan of plans) {
|
||||
const nameKey = `subscription.plans.${plan.key}.name`;
|
||||
const match = headerCells.find((th) => th.textContent?.includes(nameKey));
|
||||
expect(match, `header for ${plan.key} should render ${nameKey}`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test("each plan header shows a monthly price (non-empty header text)", () => {
|
||||
const { container } = render(<FeatureMatrix />);
|
||||
const planHeaders = Array.from(container.querySelectorAll("thead th")).slice(1);
|
||||
for (const th of planHeaders) {
|
||||
// perMonth suffix key proves the price line is present
|
||||
expect(th.textContent).toContain("common.perMonth");
|
||||
}
|
||||
});
|
||||
|
||||
test("popular plan header carries the popular badge", () => {
|
||||
const { container } = render(<FeatureMatrix />);
|
||||
const popularPlan = plans.find((p) => p.popular);
|
||||
expect(popularPlan).toBeTruthy();
|
||||
const headerCells = Array.from(container.querySelectorAll("thead th"));
|
||||
const popularHeader = headerCells.find((th) =>
|
||||
th.textContent?.includes(`subscription.plans.${popularPlan!.key}.name`),
|
||||
);
|
||||
expect(popularHeader?.textContent).toContain("subscription.popular");
|
||||
});
|
||||
|
||||
test("brand-count row exposes each plan's brand limit (the real differentiator)", () => {
|
||||
const { container } = render(<FeatureMatrix />);
|
||||
const rows = Array.from(container.querySelectorAll("tbody tr"));
|
||||
const brandRow = rows.find((r) =>
|
||||
r.querySelector("td")?.textContent?.includes("subscription.matrixBrandCount"),
|
||||
);
|
||||
expect(brandRow, "a matrixBrandCount row must exist").toBeTruthy();
|
||||
|
||||
const cells = within(brandRow as HTMLElement).getAllByRole("cell");
|
||||
// cells[0] is the row label; the rest map 1:1 to plans
|
||||
const valueCells = cells.slice(1);
|
||||
expect(valueCells).toHaveLength(plans.length);
|
||||
expect(valueCells[0].textContent).toContain("1"); // brand1
|
||||
expect(valueCells[1].textContent).toContain("2"); // brand2
|
||||
expect(valueCells[2].textContent).toContain("3"); // brand3
|
||||
expect(valueCells[3].textContent).toContain("subscription.brandCountAll"); // full
|
||||
});
|
||||
});
|
||||
@@ -1761,7 +1761,7 @@ function DowngradeOfferDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureMatrix() {
|
||||
export function FeatureMatrix() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -1774,15 +1774,40 @@ function FeatureMatrix() {
|
||||
{plans.map((plan) => (
|
||||
<th
|
||||
key={plan.key}
|
||||
className={`relative px-4 py-3 text-center font-semibold leading-5 ${
|
||||
className={`relative px-4 py-3 text-center align-bottom font-semibold leading-5 ${
|
||||
plan.popular ? "text-primary" : "text-foreground"
|
||||
}`}
|
||||
aria-label={t(`subscription.plans.${plan.key}.name`)}
|
||||
/>
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{plan.popular && (
|
||||
<Badge className="px-1.5 py-0 text-[10px]">{t("subscription.popular")}</Badge>
|
||||
)}
|
||||
<span>{t(`subscription.plans.${plan.key}.name`)}</span>
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{formatTRY(plan.priceMonthly)}
|
||||
{t("common.perMonth")}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<td className="px-4 py-3 font-medium leading-5 text-foreground">
|
||||
{t("subscription.matrixBrandCount")}
|
||||
</td>
|
||||
{plans.map((plan) => (
|
||||
<td
|
||||
key={plan.key}
|
||||
className="px-4 py-3 text-center font-semibold leading-5 text-foreground"
|
||||
>
|
||||
{plan.brandLimit === FULL_PLAN_BRAND_LIMIT
|
||||
? t("subscription.brandCountAll")
|
||||
: plan.brandLimit}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
{ALL_FEATURES.map((feature, i) => (
|
||||
<tr key={feature} className={i % 2 === 0 ? "bg-background" : "bg-muted/25"}>
|
||||
<td className="px-4 py-3 font-medium leading-5 text-foreground">
|
||||
|
||||
Reference in New Issue
Block a user