test(FN-202): add 45 regression tests for /dashboard/subscription page
Covers P0-1 through P0-10 CRO fixes and edge cases: - Rendering: plan cards, billing toggle, yearly discount (P0-1), popular visual dominance (P0-2), current plan badge (P0-5), loading state - Interaction: CTA labels (P0-3), order summary (P0-4), trial card hidden on trial status (P0-6), payment navigation, non-interactive plan - Trust & i18n: PostHog events (P0-10), i18n key usage (P0-9), trust i18n keys exist (P0-7, P0-8) - Edge cases: no subscription, expired, cancelled, empty state, full plan - Accessibility: button labels, non-clickable current plan, color - Subscription status: dates, brands, billing period, cancel button Also exports SubscriptionPage for testability and relaxes test lint rules (noNonNullAssertion, noForEach, useButtonType) in biome.json. 47/47 tests passing.
This commit is contained in:
@@ -6,29 +6,38 @@
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// ─── Pre-declare mock variables via vi.hoisted() ─────────────────────────────
|
||||
const { mockNavigate, mockCapture, mockSetPeopleProperties, mockStartAction, mockToast,
|
||||
mockInvalidateQueries, mockUseQuery, mockUseMutation, mockAuthUser } = vi.hoisted(() => ({
|
||||
mockNavigate: vi.fn(),
|
||||
mockCapture: vi.fn(),
|
||||
mockSetPeopleProperties: vi.fn(),
|
||||
mockStartAction: vi.fn(),
|
||||
mockToast: { success: vi.fn(), error: vi.fn() },
|
||||
mockInvalidateQueries: vi.fn(),
|
||||
mockUseQuery: vi.fn(),
|
||||
mockUseMutation: vi.fn(),
|
||||
mockAuthUser: {
|
||||
id: "user-1",
|
||||
name: "Test User",
|
||||
email: "test@sase.tr",
|
||||
image: null,
|
||||
role: "user",
|
||||
referralCode: "TEST123",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
}));
|
||||
const {
|
||||
mockNavigate,
|
||||
mockCapture,
|
||||
mockSetPeopleProperties,
|
||||
mockStartAction,
|
||||
mockToast,
|
||||
mockInvalidateQueries,
|
||||
mockUseQuery,
|
||||
mockUseMutation,
|
||||
mockAuthUser,
|
||||
} = vi.hoisted(() => ({
|
||||
mockNavigate: vi.fn(),
|
||||
mockCapture: vi.fn(),
|
||||
mockSetPeopleProperties: vi.fn(),
|
||||
mockStartAction: vi.fn(),
|
||||
mockToast: { success: vi.fn(), error: vi.fn() },
|
||||
mockInvalidateQueries: vi.fn(),
|
||||
mockUseQuery: vi.fn(),
|
||||
mockUseMutation: vi.fn(),
|
||||
mockAuthUser: {
|
||||
id: "user-1",
|
||||
name: "Test User",
|
||||
email: "test@sase.tr",
|
||||
image: null,
|
||||
role: "user",
|
||||
referralCode: "TEST123",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
}));
|
||||
|
||||
// ─── Mock TanStack Router ────────────────────────────────────────────────────
|
||||
vi.mock("@tanstack/react-router", async () => {
|
||||
@@ -38,7 +47,9 @@ vi.mock("@tanstack/react-router", async () => {
|
||||
useNavigate: () => mockNavigate,
|
||||
createFileRoute: () => (opts: any) => opts,
|
||||
Link: ({ children, to, ...props }: any) => (
|
||||
<a href={to} {...props}>{children}</a>
|
||||
<a href={to} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -73,9 +84,13 @@ vi.mock("@/lib/i18n", () => ({
|
||||
vi.mock("@/lib/api-client", () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), upload: vi.fn() },
|
||||
ApiError: class ApiError extends Error {
|
||||
code?: string; status?: number;
|
||||
code?: string;
|
||||
status?: number;
|
||||
constructor(message: string, code?: string, status?: number) {
|
||||
super(message); this.name = "ApiError"; this.code = code; this.status = status;
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -120,15 +135,24 @@ vi.mock("canvas-confetti", () => ({ default: vi.fn() }));
|
||||
|
||||
// ─── Mock BrandSelector ─────────────────────────────────────────────────────
|
||||
vi.mock("@/components/subscription/brand-selector", () => ({
|
||||
BrandSelector: ({ maxBrands, selectedBrandIds, onSelectionChange, isFullPlan }: {
|
||||
maxBrands: number; selectedBrandIds: string[];
|
||||
onSelectionChange: (ids: string[]) => void; isFullPlan: boolean;
|
||||
BrandSelector: ({
|
||||
maxBrands,
|
||||
selectedBrandIds,
|
||||
onSelectionChange,
|
||||
isFullPlan,
|
||||
}: {
|
||||
maxBrands: number;
|
||||
selectedBrandIds: string[];
|
||||
onSelectionChange: (ids: string[]) => void;
|
||||
isFullPlan: boolean;
|
||||
}) => (
|
||||
<div data-testid="brand-selector">
|
||||
<span data-testid="brand-selector-max">{maxBrands}</span>
|
||||
<span data-testid="brand-selector-full">{String(isFullPlan)}</span>
|
||||
<button data-testid="brand-selector-select"
|
||||
onClick={() => onSelectionChange(["brand-vw", "brand-audi"])}>
|
||||
<button
|
||||
data-testid="brand-selector-select"
|
||||
onClick={() => onSelectionChange(["brand-vw", "brand-audi"])}
|
||||
>
|
||||
Select Brands
|
||||
</button>
|
||||
</div>
|
||||
@@ -148,7 +172,9 @@ vi.mock("@/lib/keys", () => ({ BRAND_SKELETON_KEYS: ["sk1", "sk2", "sk3", "sk4"]
|
||||
// ─── Mock Remotion ──────────────────────────────────────────────────────────
|
||||
vi.mock("@remotion/player", () => ({
|
||||
default: ({ style, component: Comp, inputProps }: any) => (
|
||||
<div data-testid="remotion-player" style={style}><Comp {...inputProps} /></div>
|
||||
<div data-testid="remotion-player" style={style}>
|
||||
<Comp {...inputProps} />
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -166,8 +192,10 @@ beforeEach(() => {
|
||||
matches: query === "(prefers-color-scheme: dark)",
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(), removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
@@ -176,21 +204,29 @@ beforeEach(() => {
|
||||
// ─── Test helpers ───────────────────────────────────────────────────────────
|
||||
import { SubscriptionPage } from "@/routes/dashboard/subscription/index";
|
||||
|
||||
interface SubscriptionBrand { brandId: string; brandName: string; }
|
||||
interface SubscriptionBrand {
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
}
|
||||
interface Subscription {
|
||||
status: string; plan?: { name: string; key: string };
|
||||
billingPeriod: string; brands?: SubscriptionBrand[];
|
||||
startDate?: string; endDate?: string;
|
||||
status: string;
|
||||
plan?: { name: string; key: string };
|
||||
billingPeriod: string;
|
||||
brands?: SubscriptionBrand[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
function renderPage(options: {
|
||||
subscription?: Subscription | null;
|
||||
eligibleForTrial?: boolean;
|
||||
isLoading?: boolean;
|
||||
searchParams?: string;
|
||||
/** Custom mutate function for useMutation (for tests that need to inspect calls) */
|
||||
mutationMutate?: ReturnType<typeof vi.fn>;
|
||||
} = {}) {
|
||||
function renderPage(
|
||||
options: {
|
||||
subscription?: Subscription | null;
|
||||
eligibleForTrial?: boolean;
|
||||
isLoading?: boolean;
|
||||
searchParams?: string;
|
||||
/** Custom mutate function for useMutation (for tests that need to inspect calls) */
|
||||
mutationMutate?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
vi.clearAllMocks();
|
||||
|
||||
if (options.searchParams) {
|
||||
@@ -202,12 +238,16 @@ function renderPage(options: {
|
||||
mockUseQuery.mockImplementation(({ queryKey }: any) => {
|
||||
if (Array.isArray(queryKey) && (queryKey[1] === "me" || queryKey[0] === "subscription")) {
|
||||
return {
|
||||
data: options.isLoading ? undefined : {
|
||||
subscription: options.subscription ?? null,
|
||||
eligibleForTrial: options.eligibleForTrial ?? false,
|
||||
},
|
||||
data: options.isLoading
|
||||
? undefined
|
||||
: {
|
||||
subscription: options.subscription ?? null,
|
||||
eligibleForTrial: options.eligibleForTrial ?? false,
|
||||
},
|
||||
isLoading: options.isLoading ?? false,
|
||||
isError: false, error: null, refetch: vi.fn(),
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
};
|
||||
}
|
||||
return { data: undefined, isLoading: false, isError: false, error: null, refetch: vi.fn() };
|
||||
@@ -215,7 +255,10 @@ function renderPage(options: {
|
||||
|
||||
const mutate = options.mutationMutate || vi.fn();
|
||||
mockUseMutation.mockImplementation(() => ({
|
||||
mutate, isPending: false, isSuccess: false, isError: false,
|
||||
mutate,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
}));
|
||||
|
||||
return { ...render(<SubscriptionPage />), mutate };
|
||||
@@ -246,12 +289,24 @@ describe("rendering", () => {
|
||||
|
||||
it("renders feature lists for each plan", () => {
|
||||
renderPage();
|
||||
expect(screen.getAllByText("subscription.features.vinSearch").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.partsCatalog").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.schemaViewer").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.prioritySupport").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.oemSearch").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.allBrands").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.vinSearch").length).toBeGreaterThanOrEqual(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByText("subscription.features.partsCatalog").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
screen.getAllByText("subscription.features.schemaViewer").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
screen.getAllByText("subscription.features.prioritySupport").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.oemSearch").length).toBeGreaterThanOrEqual(
|
||||
1,
|
||||
);
|
||||
expect(screen.getAllByText("subscription.features.allBrands").length).toBeGreaterThanOrEqual(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,10 +351,12 @@ describe("rendering", () => {
|
||||
it("shows Mevcut Plan badge on current active plan card", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "1 Marka", key: "brand1" },
|
||||
status: "active",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
startDate: "2025-01-01", endDate: "2026-01-01",
|
||||
startDate: "2025-01-01",
|
||||
endDate: "2026-01-01",
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
@@ -310,7 +367,8 @@ describe("rendering", () => {
|
||||
it("current plan CTA is non-interactive", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "2 Marka", key: "brand2" },
|
||||
status: "active",
|
||||
plan: { name: "2 Marka", key: "brand2" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [
|
||||
{ brandId: "vw", brandName: "Volkswagen" },
|
||||
@@ -348,7 +406,9 @@ describe("interaction", () => {
|
||||
it("CTA says Devam Et (proceed) when plan selected", () => {
|
||||
renderPage();
|
||||
expect(screen.queryByText("subscription.proceed")).not.toBeInTheDocument();
|
||||
const brand1Card = screen.getByText("subscription.plans.brand1.name").closest('[class*="relative"]')!;
|
||||
const brand1Card = screen
|
||||
.getByText("subscription.plans.brand1.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand1Card);
|
||||
const ctaElements = screen.getAllByText(/subscription\.proceed/);
|
||||
expect(ctaElements.length).toBeGreaterThanOrEqual(1);
|
||||
@@ -356,10 +416,12 @@ describe("interaction", () => {
|
||||
|
||||
it("CTA includes price info with ile Devam Et format", () => {
|
||||
renderPage();
|
||||
const brand2Card = screen.getByText("subscription.plans.brand2.name").closest('[class*="relative"]')!;
|
||||
const brand2Card = screen
|
||||
.getByText("subscription.plans.brand2.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand2Card);
|
||||
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
|
||||
const bigCta = ctaButtons.find(b => b.textContent?.includes("₺350,00"));
|
||||
const bigCta = ctaButtons.find((b) => b.textContent?.includes("₺350,00"));
|
||||
expect(bigCta).toBeTruthy();
|
||||
expect(bigCta?.textContent).toContain("subscription.proceed");
|
||||
});
|
||||
@@ -373,21 +435,27 @@ describe("interaction", () => {
|
||||
|
||||
it("order summary renders with plan, period, and price when plan selected", () => {
|
||||
renderPage();
|
||||
const brand3Card = screen.getByText("subscription.plans.brand3.name").closest('[class*="relative"]')!;
|
||||
const brand3Card = screen
|
||||
.getByText("subscription.plans.brand3.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand3Card);
|
||||
expect(screen.getByText("subscription.orderSummary")).toBeInTheDocument();
|
||||
expect(screen.getByText("subscription.orderSummaryPlan")).toBeInTheDocument();
|
||||
expect(screen.getByText("subscription.orderSummaryPeriod")).toBeInTheDocument();
|
||||
expect(screen.getByText("subscription.orderSummaryPrice")).toBeInTheDocument();
|
||||
// Plan name appears in both card and summary — use getAllByText
|
||||
expect(screen.getAllByText("subscription.plans.brand3.name").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.plans.brand3.name").length).toBeGreaterThanOrEqual(
|
||||
1,
|
||||
);
|
||||
// common.monthly appears in both toggle button and order summary
|
||||
expect(screen.getAllByText("common.monthly").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("order summary updates when billing period changes", () => {
|
||||
renderPage();
|
||||
const brand1Card = screen.getByText("subscription.plans.brand1.name").closest('[class*="relative"]')!;
|
||||
const brand1Card = screen
|
||||
.getByText("subscription.plans.brand1.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand1Card);
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.yearly" }));
|
||||
// Yearly price appears in both card and summary
|
||||
@@ -400,8 +468,10 @@ describe("interaction", () => {
|
||||
it("does not show trial CTA when subscription status is trial", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "trial", plan: { name: "Full Paket", key: "full" },
|
||||
billingPeriod: "monthly", brands: [],
|
||||
status: "trial",
|
||||
plan: { name: "Full Paket", key: "full" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
endDate: new Date(Date.now() + 15 * 86400000).toISOString(),
|
||||
},
|
||||
eligibleForTrial: true,
|
||||
@@ -420,12 +490,14 @@ describe("interaction", () => {
|
||||
describe("proceed to payment navigation", () => {
|
||||
it("navigates to /dashboard/subscription/pay with correct search params", () => {
|
||||
renderPage();
|
||||
const brand2Card = screen.getByText("subscription.plans.brand2.name").closest('[class*="relative"]')!;
|
||||
const brand2Card = screen
|
||||
.getByText("subscription.plans.brand2.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand2Card);
|
||||
fireEvent.click(screen.getByTestId("brand-selector-select"));
|
||||
// Click the big CTA (not the small card button)
|
||||
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
|
||||
const bigCta = ctaButtons.find(b => b.textContent?.includes("₺"))!;
|
||||
const bigCta = ctaButtons.find((b) => b.textContent?.includes("₺"))!;
|
||||
fireEvent.click(bigCta);
|
||||
expect(mockNavigate).toHaveBeenCalledWith({
|
||||
to: "/dashboard/subscription/pay",
|
||||
@@ -439,10 +511,12 @@ describe("interaction", () => {
|
||||
|
||||
it("shows error toast when proceeding without brand selection", () => {
|
||||
renderPage();
|
||||
const brand1Card = screen.getByText("subscription.plans.brand1.name").closest('[class*="relative"]')!;
|
||||
const brand1Card = screen
|
||||
.getByText("subscription.plans.brand1.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand1Card);
|
||||
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
|
||||
const bigCta = ctaButtons.find(b => b.textContent?.includes("₺"))!;
|
||||
const bigCta = ctaButtons.find((b) => b.textContent?.includes("₺"))!;
|
||||
fireEvent.click(bigCta);
|
||||
expect(mockToast.error).toHaveBeenCalledWith("subscription.selectBrandsDescription");
|
||||
});
|
||||
@@ -452,7 +526,8 @@ describe("interaction", () => {
|
||||
it("clicking current plan card does not select it", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "2 Marka", key: "brand2" },
|
||||
status: "active",
|
||||
plan: { name: "2 Marka", key: "brand2" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [
|
||||
{ brandId: "vw", brandName: "Volkswagen" },
|
||||
@@ -461,14 +536,18 @@ describe("interaction", () => {
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
const brand1Card = screen.getByText("subscription.plans.brand1.name").closest('[class*="relative"]')!;
|
||||
const brand1Card = screen
|
||||
.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 => {
|
||||
if (card.textContent?.includes("subscription.plans.brand2.name") &&
|
||||
card.className.includes("border-green")) {
|
||||
cards.forEach((card) => {
|
||||
if (
|
||||
card.textContent?.includes("subscription.plans.brand2.name") &&
|
||||
card.className.includes("border-green")
|
||||
) {
|
||||
if (!card.className.includes("cursor-pointer")) foundNonClickable = true;
|
||||
}
|
||||
});
|
||||
@@ -479,7 +558,9 @@ describe("interaction", () => {
|
||||
describe("plan selection persists across billing toggle", () => {
|
||||
it("selected plan stays selected after switching billing period", () => {
|
||||
renderPage();
|
||||
const brand3Card = screen.getByText("subscription.plans.brand3.name").closest('[class*="relative"]')!;
|
||||
const brand3Card = screen
|
||||
.getByText("subscription.plans.brand3.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand3Card);
|
||||
expect(screen.getByText("subscription.orderSummary")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.yearly" }));
|
||||
@@ -496,11 +577,13 @@ describe("trust and i18n", () => {
|
||||
describe("PostHog events (P0-10)", () => {
|
||||
it("fires checkout_started event when proceeding to payment", () => {
|
||||
renderPage();
|
||||
const brand2Card = screen.getByText("subscription.plans.brand2.name").closest('[class*="relative"]')!;
|
||||
const brand2Card = screen
|
||||
.getByText("subscription.plans.brand2.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand2Card);
|
||||
fireEvent.click(screen.getByTestId("brand-selector-select"));
|
||||
const ctaButtons = screen.getAllByRole("button", { name: /subscription\.proceed/ });
|
||||
fireEvent.click(ctaButtons.find(b => b.textContent?.includes("₺"))!);
|
||||
fireEvent.click(ctaButtons.find((b) => b.textContent?.includes("₺"))!);
|
||||
expect(mockCapture).toHaveBeenCalledWith(
|
||||
"checkout_started",
|
||||
expect.objectContaining({ plan: "brand2", period: "monthly" }),
|
||||
@@ -509,7 +592,9 @@ describe("trust and i18n", () => {
|
||||
|
||||
it("fires plan_selected event when clicking a plan card", () => {
|
||||
renderPage();
|
||||
const brand3Card = screen.getByText("subscription.plans.brand3.name").closest('[class*="relative"]')!;
|
||||
const brand3Card = screen
|
||||
.getByText("subscription.plans.brand3.name")
|
||||
.closest('[class*="relative"]')!;
|
||||
fireEvent.click(brand3Card);
|
||||
expect(mockCapture).toHaveBeenCalledWith("plan_selected", { plan: "brand3" });
|
||||
});
|
||||
@@ -542,7 +627,8 @@ describe("trust and i18n", () => {
|
||||
it("status labels use i18n keys", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "1 Marka", key: "brand1" },
|
||||
status: "active",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
},
|
||||
@@ -556,9 +642,12 @@ describe("trust and i18n", () => {
|
||||
it("trust-related i18n keys are available", async () => {
|
||||
const { t: realT } = await vi.importActual<any>("@/lib/i18n");
|
||||
const keys = [
|
||||
"subscription.trustNoCard", "subscription.trustCancelAnytime",
|
||||
"subscription.trustRefund", "subscription.paymentTrustSSL",
|
||||
"subscription.paymentTrustProvider", "subscription.paymentTrustKVKK",
|
||||
"subscription.trustNoCard",
|
||||
"subscription.trustCancelAnytime",
|
||||
"subscription.trustRefund",
|
||||
"subscription.paymentTrustSSL",
|
||||
"subscription.paymentTrustProvider",
|
||||
"subscription.paymentTrustKVKK",
|
||||
];
|
||||
for (const key of keys) {
|
||||
const result = realT(key);
|
||||
@@ -579,7 +668,9 @@ describe("edge cases", () => {
|
||||
renderPage({ subscription: null, eligibleForTrial: true });
|
||||
expect(screen.getByText("subscription.trialTitle")).toBeInTheDocument();
|
||||
expect(screen.getByText("subscription.trialDescription")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("subscription.features.allBrands").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("subscription.features.allBrands").length).toBeGreaterThanOrEqual(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("start trial button triggers trial mutation", () => {
|
||||
@@ -593,7 +684,12 @@ describe("edge cases", () => {
|
||||
describe("expired subscription", () => {
|
||||
it("shows trial CTA when expired and eligible", () => {
|
||||
renderPage({
|
||||
subscription: { status: "expired", plan: { name: "1 Marka", key: "brand1" }, billingPeriod: "monthly", brands: [] },
|
||||
subscription: {
|
||||
status: "expired",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
},
|
||||
eligibleForTrial: true,
|
||||
});
|
||||
expect(screen.getByText("subscription.trialTitle")).toBeInTheDocument();
|
||||
@@ -601,7 +697,12 @@ describe("edge cases", () => {
|
||||
|
||||
it("does not show trial CTA when expired but not eligible", () => {
|
||||
renderPage({
|
||||
subscription: { status: "expired", plan: { name: "1 Marka", key: "brand1" }, billingPeriod: "monthly", brands: [] },
|
||||
subscription: {
|
||||
status: "expired",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
expect(screen.queryByText("subscription.trialTitle")).not.toBeInTheDocument();
|
||||
@@ -619,14 +720,16 @@ describe("edge cases", () => {
|
||||
it("shows current plan info for full plan users", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "Full Paket", key: "full" },
|
||||
status: "active",
|
||||
plan: { name: "Full Paket", key: "full" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [
|
||||
{ brandId: "vw", brandName: "Volkswagen" },
|
||||
{ brandId: "audi", brandName: "Audi" },
|
||||
{ brandId: "bmw", brandName: "BMW" },
|
||||
],
|
||||
startDate: "2025-01-01", endDate: "2026-01-01",
|
||||
startDate: "2025-01-01",
|
||||
endDate: "2026-01-01",
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
@@ -639,7 +742,12 @@ describe("edge cases", () => {
|
||||
describe("cancelled subscription", () => {
|
||||
it("shows resume button for cancelled subscriptions", () => {
|
||||
renderPage({
|
||||
subscription: { status: "cancelled", plan: { name: "2 Marka", key: "brand2" }, billingPeriod: "monthly", brands: [] },
|
||||
subscription: {
|
||||
status: "cancelled",
|
||||
plan: { name: "2 Marka", key: "brand2" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
const resumeBtn = screen.queryByText("subscription.resumeSubscription");
|
||||
@@ -651,8 +759,10 @@ describe("edge cases", () => {
|
||||
it("does not show trial card when user is on trial", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "trial", plan: { name: "Full Paket", key: "full" },
|
||||
billingPeriod: "monthly", brands: [],
|
||||
status: "trial",
|
||||
plan: { name: "Full Paket", key: "full" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
endDate: new Date(Date.now() + 10 * 86400000).toISOString(),
|
||||
},
|
||||
eligibleForTrial: true,
|
||||
@@ -672,13 +782,14 @@ describe("accessibility", () => {
|
||||
it("CTA buttons have accessible labels", () => {
|
||||
renderPage();
|
||||
const buttons = screen.getAllByRole("button", { name: /subscription\.(choosePlan|proceed)/ });
|
||||
buttons.forEach(b => expect(b.textContent).toBeTruthy());
|
||||
buttons.forEach((b) => expect(b.textContent).toBeTruthy());
|
||||
});
|
||||
|
||||
it("current plan card is not clickable", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "2 Marka", key: "brand2" },
|
||||
status: "active",
|
||||
plan: { name: "2 Marka", key: "brand2" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
},
|
||||
@@ -686,17 +797,20 @@ describe("accessibility", () => {
|
||||
});
|
||||
const cards = document.querySelectorAll('[class*="relative"]');
|
||||
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;
|
||||
cards.forEach((card) => {
|
||||
if (
|
||||
card.textContent?.includes("subscription.plans.brand2.name") &&
|
||||
card.className.includes("border-green") &&
|
||||
!card.className.includes("cursor-pointer")
|
||||
)
|
||||
found = true;
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it("plan cards have visible names", () => {
|
||||
renderPage();
|
||||
["brand1", "brand2", "brand3", "full"].forEach(key => {
|
||||
["brand1", "brand2", "brand3", "full"].forEach((key) => {
|
||||
expect(screen.getByText(`subscription.plans.${key}.name`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -718,10 +832,12 @@ describe("subscription status display", () => {
|
||||
it("shows subscription info card with dates and brands", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "1 Marka", key: "brand1" },
|
||||
status: "active",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "yearly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
startDate: "2025-06-01", endDate: "2026-06-01",
|
||||
startDate: "2025-06-01",
|
||||
endDate: "2026-06-01",
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
@@ -737,7 +853,8 @@ describe("subscription status display", () => {
|
||||
it("shows billing period in subscription card description", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "1 Marka", key: "brand1" },
|
||||
status: "active",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "yearly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
},
|
||||
@@ -749,7 +866,12 @@ describe("subscription status display", () => {
|
||||
|
||||
it("does not show cancel button for non-active subscriptions", () => {
|
||||
renderPage({
|
||||
subscription: { status: "cancelled", plan: { name: "1 Marka", key: "brand1" }, billingPeriod: "monthly", brands: [] },
|
||||
subscription: {
|
||||
status: "cancelled",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [],
|
||||
},
|
||||
eligibleForTrial: false,
|
||||
});
|
||||
expect(screen.queryByText("subscription.cancelSubscription")).not.toBeInTheDocument();
|
||||
@@ -758,7 +880,8 @@ describe("subscription status display", () => {
|
||||
it("shows cancel button only for active subscriptions", () => {
|
||||
renderPage({
|
||||
subscription: {
|
||||
status: "active", plan: { name: "1 Marka", key: "brand1" },
|
||||
status: "active",
|
||||
plan: { name: "1 Marka", key: "brand1" },
|
||||
billingPeriod: "monthly",
|
||||
brands: [{ brandId: "vw", brandName: "Volkswagen" }],
|
||||
},
|
||||
|
||||
@@ -16,6 +16,15 @@
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"useButtonType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user