From e94c1e426744c9b8852df0496681786fd7b4414a Mon Sep 17 00:00:00 2001 From: Fusion Date: Wed, 13 May 2026 00:32:50 +0000 Subject: [PATCH] feat(FN-271): implement a11y fixes (plan card focus, trust bar ARIA, color contrast) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../subscription/__tests__/index.test.tsx | 87 ++++++++++------- .../routes/dashboard/subscription/index.tsx | 94 +++++++++++++++---- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/apps/web/src/routes/dashboard/subscription/__tests__/index.test.tsx b/apps/web/src/routes/dashboard/subscription/__tests__/index.test.tsx index fca277e..3be2e75 100644 --- a/apps/web/src/routes/dashboard/subscription/__tests__/index.test.tsx +++ b/apps/web/src/routes/dashboard/subscription/__tests__/index.test.tsx @@ -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); }); diff --git a/apps/web/src/routes/dashboard/subscription/index.tsx b/apps/web/src/routes/dashboard/subscription/index.tsx index 39754c8..a313105 100644 --- a/apps/web/src/routes/dashboard/subscription/index.tsx +++ b/apps/web/src/routes/dashboard/subscription/index.tsx @@ -803,6 +803,20 @@ export function SubscriptionPage() { {trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")} +
+ + + {t("subscription.trustNoCard")} + + + + {t("subscription.trustCancelAnytime")} + + + + {t("subscription.trustRefund")} + +
)} @@ -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 && ( @@ -921,6 +932,8 @@ export function SubscriptionPage() { @@ -1011,19 +1024,68 @@ export function SubscriptionPage() { isFullPlan={selectedPlanKey === "full"} /> -
- +
+
+ +
+
+ + {t("subscription.paymentTrustSSL")} + + {t("subscription.paymentTrustProvider")} + + + {t("subscription.paymentTrustKVKK")} + + + + + + + +