feat(FN-271): implement a11y fixes (plan card focus, trust bar ARIA, color contrast)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- feat(FN-271): complete Step 1 — implement a11y fixes (plan card focus, trust bar ARIA, color contrast)

Files changed:
.../subscription/__tests__/index.test.tsx          | 87 ++++++++++++--------
 .../src/routes/dashboard/subscription/index.tsx    | 94 ++++++++++++++++++----
 2 files changed, 133 insertions(+), 48 deletions(-)

Fusion-Task-Id: FN-271
This commit is contained in:
Fusion
2026-05-13 00:32:50 +00:00
parent f9460d2dd4
commit e94c1e4267
2 changed files with 133 additions and 48 deletions

View File

@@ -427,8 +427,10 @@ describe("interaction", () => {
const gridScope = within(planGrid as HTMLElement);
const brand1Card = gridScope
.getByText("subscription.plans.brand1.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand1Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand1Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
const ctaElements = screen.getAllByText(/subscription\.proceed/);
expect(ctaElements.length).toBeGreaterThanOrEqual(1);
});
@@ -439,8 +441,10 @@ describe("interaction", () => {
const gridScope = within(planGrid as HTMLElement);
const brand2Card = gridScope
.getByText("subscription.plans.brand2.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand2Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand2Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
const bigCta = ctaButtons.find((b) => b.textContent?.includes("₺350,00"));
expect(bigCta).toBeTruthy();
@@ -460,8 +464,10 @@ describe("interaction", () => {
const gridScope = within(planGrid as HTMLElement);
const brand3Card = gridScope
.getByText("subscription.plans.brand3.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand3Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand3Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
expect(screen.getByText("subscription.orderSummary")).toBeInTheDocument();
expect(screen.getByText("subscription.orderSummaryPlan")).toBeInTheDocument();
expect(screen.getByText("subscription.orderSummaryPeriod")).toBeInTheDocument();
@@ -480,8 +486,10 @@ describe("interaction", () => {
const gridScope1 = within(planGrid1 as HTMLElement);
const brand1Card = gridScope1
.getByText("subscription.plans.brand1.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand1Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand1Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
fireEvent.click(screen.getByRole("button", { name: "common.yearly" }));
// Yearly price appears in both card and summary
const summaryPrices = screen.getAllByText("₺2.000,00");
@@ -519,8 +527,10 @@ describe("interaction", () => {
const gridScopeNav = within(planGridNav as HTMLElement);
const brand2Card = gridScopeNav
.getByText("subscription.plans.brand2.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand2Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand2Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
fireEvent.click(screen.getByTestId("brand-selector-select"));
// Click the big CTA (not the small card button)
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
@@ -542,8 +552,10 @@ describe("interaction", () => {
const gridScopeErr = within(planGridErr as HTMLElement);
const brand1Card = gridScopeErr
.getByText("subscription.plans.brand1.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand1Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand1Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
const bigCta = ctaButtons.find((b) => b.textContent?.includes("₺"))!;
fireEvent.click(bigCta);
@@ -567,22 +579,26 @@ describe("interaction", () => {
});
const planGridCur = document.querySelector(".grid.gap-4");
const gridScopeCur = within(planGridCur as HTMLElement);
// brand1 card should have a selectable button (not current plan)
const brand1Card = gridScopeCur
.getByText("subscription.plans.brand1.name")
.closest('[class*="relative"]')!;
expect(brand1Card.className).toContain("cursor-pointer");
// Current plan card should NOT be clickable
const cards = document.querySelectorAll('[class*="relative"]');
let foundNonClickable = false;
cards.forEach((card) => {
.closest('[class*="rounded-xl"]')!;
const brand1Button = within(brand1Card as HTMLElement).queryByRole("button");
expect(brand1Button).toBeTruthy();
// Current plan (brand2) should NOT have a button — it shows a non-interactive div instead
const currentPlanCards = document.querySelectorAll('[class*="rounded-xl"]');
let currentPlanHasNoButton = false;
currentPlanCards.forEach((card) => {
if (
card.textContent?.includes("subscription.plans.brand2.name") &&
card.className.includes("border-green")
) {
if (!card.className.includes("cursor-pointer")) foundNonClickable = true;
// Current plan card should have a div with "Mevcut Plan" text, not a button
const btn = card.querySelector("button");
if (!btn) currentPlanHasNoButton = true;
}
});
expect(foundNonClickable).toBe(true);
expect(currentPlanHasNoButton).toBe(true);
});
});
@@ -593,8 +609,10 @@ describe("interaction", () => {
const gridScopePersist = within(planGridPersist as HTMLElement);
const brand3Card = gridScopePersist
.getByText("subscription.plans.brand3.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand3Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand3Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
expect(screen.getByText("subscription.orderSummary")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "common.yearly" }));
expect(screen.getByText("subscription.orderSummary")).toBeInTheDocument();
@@ -614,8 +632,10 @@ describe("trust and i18n", () => {
const gridScopePH1 = within(planGridPH1 as HTMLElement);
const brand2Card = gridScopePH1
.getByText("subscription.plans.brand2.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand2Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand2Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
fireEvent.click(screen.getByTestId("brand-selector-select"));
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
fireEvent.click(ctaButtons.find((b) => b.textContent?.includes("₺"))!);
@@ -625,14 +645,16 @@ describe("trust and i18n", () => {
);
});
it("fires plan_selected event when clicking a plan card", () => {
it("fires plan_selected event when clicking a plan card button", () => {
renderPage();
const planGridPH2 = document.querySelector(".grid.gap-4");
const gridScopePH2 = within(planGridPH2 as HTMLElement);
const brand3Card = gridScopePH2
.getByText("subscription.plans.brand3.name")
.closest('[class*="relative"]')!;
fireEvent.click(brand3Card);
.closest('[class*="rounded-xl"]')!;
fireEvent.click(
within(brand3Card as HTMLElement).getByRole("button", { name: "subscription.choosePlan" }),
);
expect(mockCapture).toHaveBeenCalledWith("plan_selected", { plan: "brand3" });
});
@@ -832,15 +854,16 @@ describe("accessibility", () => {
},
eligibleForTrial: false,
});
const cards = document.querySelectorAll('[class*="relative"]');
const cards = document.querySelectorAll('[class*="rounded-xl"]');
let found = false;
cards.forEach((card) => {
if (
card.textContent?.includes("subscription.plans.brand2.name") &&
card.className.includes("border-green") &&
!card.className.includes("cursor-pointer")
)
found = true;
card.className.includes("border-green")
) {
// Current plan card should not have a button element
if (!card.querySelector("button")) found = true;
}
});
expect(found).toBe(true);
});

View File

@@ -803,6 +803,20 @@ export function SubscriptionPage() {
<ShieldCheck className="mr-2 h-4 w-4" />
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
</Button>
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-foreground/70">
<span className="flex items-center gap-1">
<CreditCard className="h-3 w-3 shrink-0" />
{t("subscription.trustNoCard")}
</span>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3 shrink-0" />
{t("subscription.trustCancelAnytime")}
</span>
<span className="flex items-center gap-1">
<ShieldCheck className="h-3 w-3 shrink-0" />
{t("subscription.trustRefund")}
</span>
</div>
</CardContent>
</Card>
)}
@@ -857,8 +871,6 @@ export function SubscriptionPage() {
key={plan.key}
data-faro-user-action-name={`select-plan-${plan.key}`}
className={`relative transition-all hover:shadow-md ${
isCurrentPlan ? "" : "cursor-pointer"
} ${
isSelected && !isCurrentPlan
? "border-primary ring-2 ring-primary/20 dark:ring-primary/25"
: ""
@@ -867,7 +879,6 @@ export function SubscriptionPage() {
? "border-primary/40 dark:border-primary/25 ring-2 ring-primary/25 dark:ring-primary/20 shadow-brand bg-primary/[0.07] dark:bg-primary/[0.10]"
: ""
}`}
onClick={isCurrentPlan ? undefined : () => handleSelectPlan(plan.key)}
>
{plan.popular && (
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2">
@@ -921,6 +932,8 @@ export function SubscriptionPage() {
<Button
className="w-full"
variant={isSelected ? "default" : plan.popular ? "default" : "outline"}
aria-pressed={isSelected}
onClick={() => handleSelectPlan(plan.key)}
>
{isSelected ? t("subscription.proceed") : t("subscription.choosePlan")}
</Button>
@@ -1011,19 +1024,68 @@ export function SubscriptionPage() {
isFullPlan={selectedPlanKey === "full"}
/>
</Suspense>
<div className="flex justify-end">
<Button size="lg" onClick={handleProceedToPayment}>
{(() => {
const proceedPlan = plans.find((p) => p.key === selectedPlanKey);
const proceedPrice =
billingPeriod === "monthly"
? (proceedPlan?.priceMonthly ?? 0)
: (proceedPlan?.priceYearly ?? 0);
const proceedPeriodLabel =
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
return `${formatTRY(proceedPrice)}${proceedPeriodLabel} ile ${t("subscription.proceed")}`;
})()}
</Button>
<div className="space-y-3">
<div className="flex justify-end">
<Button size="lg" onClick={handleProceedToPayment}>
{(() => {
const proceedPlan = plans.find((p) => p.key === selectedPlanKey);
const proceedPrice =
billingPeriod === "monthly"
? (proceedPlan?.priceMonthly ?? 0)
: (proceedPlan?.priceYearly ?? 0);
const proceedPeriodLabel =
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
return `${formatTRY(proceedPrice)}${proceedPeriodLabel} ile ${t("subscription.proceed")}`;
})()}
</Button>
</div>
<section
aria-label="Ödeme güvencesi"
className="flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-xs text-foreground/70"
>
<span aria-hidden="true">🔒</span>
<span>{t("subscription.paymentTrustSSL")}</span>
<span aria-hidden="true">·</span>
<span>{t("subscription.paymentTrustProvider")}</span>
<span aria-hidden="true">·</span>
<a href="/kvkk" className="underline underline-offset-2 hover:text-foreground">
{t("subscription.paymentTrustKVKK")}
</a>
<span
role="img"
aria-label="Kabul edilen kartlar: Visa, Mastercard, Troy, American Express"
className="ml-1 flex items-center gap-1"
>
<Badge
variant="outline"
className="px-1.5 py-0 text-[10px] font-normal"
aria-hidden="true"
>
Visa
</Badge>
<Badge
variant="outline"
className="px-1.5 py-0 text-[10px] font-normal"
aria-hidden="true"
>
Mastercard
</Badge>
<Badge
variant="outline"
className="px-1.5 py-0 text-[10px] font-normal"
aria-hidden="true"
>
Troy
</Badge>
<Badge
variant="outline"
className="px-1.5 py-0 text-[10px] font-normal"
aria-hidden="true"
>
AmEx
</Badge>
</span>
</section>
</div>
</div>
</>