feat(FN-245): add 20 regression tests for subscription page remaining coverage
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- test(FN-245): add 20 regression tests for subscription page remaining coverage

Files changed:
.../routes/dashboard/subscription/index.test.tsx   | 382 ++++++++++++++++++++-
 1 file changed, 379 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-245
This commit is contained in:
Fusion
2026-05-12 19:46:11 +00:00
parent 68b4abe858
commit 668ab50cf2

View File

@@ -1,8 +1,9 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
/**
* Tests for the SubscriptionPage including FN-206 Feature Matrix.
*/
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { act } from "react";
import { vi } from "vitest";
import { SubscriptionPage } from "./index";
@@ -45,6 +46,7 @@ vi.mock("@/lib/posthog", () => ({
capture: vi.fn(),
setPeopleProperties: vi.fn(),
}));
import { capture } from "@/lib/posthog";
// Mock @/lib/faro
vi.mock("@/lib/faro", () => ({
@@ -66,7 +68,7 @@ vi.mock("canvas-confetti", () => ({ default: vi.fn() }));
// Mock lazy BrandSelector
vi.mock("@/components/subscription/brand-selector", () => ({
BrandSelector: () => null,
BrandSelector: () => <div data-testid="brand-selector">BrandSelector</div>,
}));
// Mock @remotion/player
@@ -93,6 +95,9 @@ function renderWithProviders(ui: React.ReactElement) {
beforeEach(() => {
vi.clearAllMocks();
mockNavigate.mockClear();
// Reset URL params to avoid cross-test contamination from onboarding flow:
window.history.replaceState({}, "", "/dashboard/subscription");
});
// ═══════════════════════════════════════════════════════════════════════════════
@@ -175,3 +180,374 @@ describe("FN-206 — Feature matrix comparison table", () => {
expect(brand1OemCell.querySelector('[aria-label="Hayır"]')).toBeInTheDocument();
});
});
// ── Fixtures ─────────────────────────────────────────────────────────────────
const fixtures = {
// No subscription, not trial-eligible — shows plan cards + no-sub banner
noSub: { subscription: null, eligibleForTrial: false },
// No subscription, trial-eligible — shows trial CTA card
trialEligible: { subscription: null, eligibleForTrial: true },
// Active subscriber on brand1 (monthly)
activeBrand1: {
subscription: {
status: "active",
plan: { name: "1 Marka", key: "brand1" },
billingPeriod: "monthly",
brands: [{ brandId: "b1", brandName: "Volkswagen" }],
startDate: "2026-01-01",
endDate: "2026-12-31",
},
eligibleForTrial: false,
},
// Trial user (5 days left)
trialActive: {
subscription: {
status: "trial",
plan: { name: "Full Paket", key: "full" },
billingPeriod: "monthly",
brands: [],
startDate: new Date().toISOString(),
endDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
eligibleForTrial: false,
},
// Cancelled subscription
cancelled: {
subscription: {
status: "cancelled",
plan: { name: "1 Marka", key: "brand1" },
billingPeriod: "monthly",
brands: [],
startDate: "2026-01-01",
endDate: "2026-06-01",
},
eligibleForTrial: false,
},
// Expired subscription, not trial-eligible
expired: {
subscription: {
status: "expired",
plan: { name: "1 Marka", key: "brand1" },
billingPeriod: "monthly",
brands: [],
startDate: "2025-01-01",
endDate: "2025-12-31",
},
eligibleForTrial: false,
},
};
// ═══════════════════════════════════════════════════════════════════════════════
// Group A — Plan Card Rendering (P0-1, P0-2)
// ═══════════════════════════════════════════════════════════════════════════════
describe("P0-1 / P0-2 — Plan card rendering", () => {
test("renders all 4 plan card names in the grid", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
// Names appear in both plan cards and feature matrix table headers
await waitFor(() => expect(screen.getAllByText("1 Marka").length).toBeGreaterThanOrEqual(1));
expect(screen.getAllByText("2 Marka").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("3 Marka").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Full Paket").length).toBeGreaterThanOrEqual(1);
});
test("renders monthly prices for all plans by default", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("1 Marka").length).toBeGreaterThanOrEqual(1));
// brand1 monthly price: 20000 / 100 = 200 → ₺200,00
expect(screen.getByText(/₺200,00/)).toBeInTheDocument();
});
test("P0-1: yearly toggle switches prices and shows 17% indirim badge on every card", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("1 Marka").length).toBeGreaterThanOrEqual(1));
await act(async () => {
screen.getByText("Yıllık").click();
});
// 17% indirim badge appears (one per plan card)
const discountBadges = screen.getAllByText("17% indirim");
expect(discountBadges.length).toBe(4);
// brand1 yearly price: 200000 / 100 = 2000 → ₺2.000,00
expect(screen.getByText(/₺2\.000,00/)).toBeInTheDocument();
});
test("P0-2: brand2 card has Popüler badge (visual dominance indicator)", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
// "Popüler" appears in both plan card badge and feature matrix table header
await waitFor(() => expect(screen.getAllByText("Popüler").length).toBeGreaterThanOrEqual(2));
});
test("full plan shows allBrands feature; brand1 does not", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("1 Marka").length).toBeGreaterThanOrEqual(1));
// "Tüm markalar" should appear at least once (full plan)
const allBrandsFeature = screen.getAllByText(/tüm marka/i);
expect(allBrandsFeature.length).toBeGreaterThanOrEqual(1);
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group B — Current Plan Badge (P0-5)
// ═══════════════════════════════════════════════════════════════════════════════
describe("P0-5 — Current active plan is non-interactive", () => {
test("active subscriber sees Mevcut Plan badge and no Plan Seç button for their plan", async () => {
apiGet.mockResolvedValue(fixtures.activeBrand1);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
const badges = screen.getAllByText("Mevcut Plan");
expect(badges.length).toBeGreaterThanOrEqual(1);
});
// There should be 3 "Plan Seç" buttons (for brand2, brand3, full) but NOT for brand1
const choosePlanButtons = screen.getAllByText("Plan Seç");
expect(choosePlanButtons).toHaveLength(3);
});
test("P0-5: clicking current plan card does not trigger plan selection", async () => {
apiGet.mockResolvedValue(fixtures.activeBrand1);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
screen.getAllByText("Mevcut Plan");
});
// The brand-selector section should NOT appear (no plan selected)
expect(screen.queryByTestId("brand-selector")).not.toBeInTheDocument();
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group C — Plan Selection Interaction (P0-3, P0-4)
// ═══════════════════════════════════════════════════════════════════════════════
describe("P0-3 / P0-4 — Plan selection interaction", () => {
test("P0-3: selecting a plan changes button label from Plan Seç to Devam Et", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
const buttons = screen.getAllByText("Plan Seç");
await act(async () => {
buttons[0].click(); // select brand1
});
// The clicked card's button becomes "Devam Et"; others stay "Plan Seç"
await waitFor(() => {
expect(screen.getByText("Devam Et")).toBeInTheDocument();
expect(screen.getAllByText("Plan Seç").length).toBe(3);
});
});
test("P0-4: selecting a plan shows order summary with plan name, period, and price", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
await act(async () => {
screen.getAllByText("Plan Seç")[0].click(); // select brand1
});
await waitFor(() => {
// Order summary card is visible
expect(screen.getByText(/sipariş özeti/i)).toBeInTheDocument();
});
// Summary shows selected plan name
// brand1 name appears at least twice: once in plan grid, once in summary
const brand1Occurrences = screen.getAllByText("1 Marka");
expect(brand1Occurrences.length).toBeGreaterThanOrEqual(2);
});
test("plan selection persists after switching billing period", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
// Select brand2
await act(async () => {
screen.getAllByText("Plan Seç")[1].click();
});
await waitFor(() => expect(screen.getByText("Devam Et")).toBeInTheDocument());
// Switch to yearly
await act(async () => {
screen.getByText("Yıllık").click();
});
// "Devam Et" is still present (selection was preserved)
await waitFor(() => {
expect(screen.getByText("Devam Et")).toBeInTheDocument();
});
});
test("plan_selected PostHog event fires when clicking a plan card", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
await act(async () => {
screen.getAllByText("Plan Seç")[0].click();
});
expect(capture).toHaveBeenCalledWith("plan_selected", { plan: "brand1" });
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group D — CTA Navigation + PostHog (P0-10 completion)
// ═══════════════════════════════════════════════════════════════════════════════
describe("P0-10 (part 2) — checkout_started event and navigation", () => {
test("CTA click on full plan navigates to /dashboard/subscription/pay", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
// Select full plan (index 3) — full plan bypasses brand validation
await act(async () => {
screen.getAllByText("Plan Seç")[3].click();
});
await waitFor(() => expect(screen.getByTestId("brand-selector")).toBeInTheDocument());
// Click proceed CTA (the bottom-right navigation button)
await act(async () => {
const proceedButtons = screen
.getAllByRole("button")
.filter((b) => b.textContent?.includes("Devam Et"));
// Use last — card footer button re-triggers selection; bottom button navigates
proceedButtons[proceedButtons.length - 1].click();
});
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith(
expect.objectContaining({
to: "/dashboard/subscription/pay",
search: expect.objectContaining({ plan: "full", period: "monthly" }),
}),
);
});
});
test("checkout_started PostHog event fires with plan and period on CTA click", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
await act(async () => {
screen.getAllByText("Plan Seç")[3].click(); // full plan
});
await waitFor(() => expect(screen.getByTestId("brand-selector")).toBeInTheDocument());
await act(async () => {
const proceedButtons = screen
.getAllByRole("button")
.filter((b) => b.textContent?.includes("Devam Et"));
proceedButtons[proceedButtons.length - 1].click();
});
await waitFor(() => {
expect(capture).toHaveBeenCalledWith("checkout_started", {
plan: "full",
period: "monthly",
});
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group E — Trial Card Visibility (P0-6)
// ═══════════════════════════════════════════════════════════════════════════════
describe("P0-6 — Trial card visibility", () => {
test("trial CTA card visible when no subscription and eligible for trial", async () => {
apiGet.mockResolvedValue(fixtures.trialEligible);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
expect(screen.getByText("Ücretsiz Denemeyi Başlat")).toBeInTheDocument();
});
});
test("P0-6: trial CTA card hidden when subscription.status is trial", async () => {
apiGet.mockResolvedValue(fixtures.trialActive);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
const badges = screen.getAllByText("Mevcut Plan");
expect(badges.length).toBeGreaterThanOrEqual(1);
});
expect(screen.queryByText("Ücretsiz Denemeyi Başlat")).not.toBeInTheDocument();
});
test("trial CTA hidden for expired subscription that is not trial-eligible", async () => {
apiGet.mockResolvedValue(fixtures.expired);
renderWithProviders(<SubscriptionPage />);
// "Full Paket" appears in both plan card and feature matrix table header
await waitFor(() => expect(screen.getAllByText("Full Paket").length).toBeGreaterThanOrEqual(1));
expect(screen.queryByText("Ücretsiz Denemeyi Başlat")).not.toBeInTheDocument();
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group F — Edge Cases
// ═══════════════════════════════════════════════════════════════════════════════
describe("Edge cases — subscription states", () => {
test("no-subscription banner visible when not eligible for trial and no subscription", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
expect(screen.getByText("Aktif aboneliğiniz yok.")).toBeInTheDocument();
});
});
test("cancelled subscription shows Aboneliği Devam Ettir button", async () => {
apiGet.mockResolvedValue(fixtures.cancelled);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => {
expect(screen.getByText("Aboneliği Devam Ettir")).toBeInTheDocument();
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// Group G — Accessibility
// ═══════════════════════════════════════════════════════════════════════════════
describe("Accessibility", () => {
test("plan selection CTA buttons have descriptive labels (not icon-only)", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
await waitFor(() => expect(screen.getAllByText("Plan Seç").length).toBe(4));
const buttons = screen.getAllByRole("button", { name: /plan seç/i });
expect(buttons.length).toBeGreaterThanOrEqual(1);
});
test("popular plan is identifiable by text (Popüler badge), not just color", async () => {
apiGet.mockResolvedValue(fixtures.noSub);
renderWithProviders(<SubscriptionPage />);
// "Popüler" appears in both plan card badge and feature matrix table header
await waitFor(() => expect(screen.getAllByText("Popüler").length).toBeGreaterThanOrEqual(2));
// Verify the badge elements are present (two instances expected)
const popularBadges = screen.getAllByText("Popüler");
expect(popularBadges.length).toBeGreaterThanOrEqual(2);
});
});