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
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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user