feat(FN-346): add CLS regression test for skeleton plan grid (+1 more)

Commits merged:
- feat(FN-346): add grid-parity integration test for skeleton CLS regression
- feat(FN-346): add CLS regression test for skeleton plan grid

Files changed:
.../subscription/__tests__/cls-regression.test.tsx | 128 +++++++++++++++++++
 .../subscription/__tests__/grid-parity.test.tsx    | 141 +++++++++++++++++++++
 .../src/routes/dashboard/subscription/index.tsx    |   4 +-
 3 files changed, 271 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-346
This commit is contained in:
Fusion
2026-05-14 05:09:39 +00:00
parent 5333ef44c7
commit 906eef148d
3 changed files with 271 additions and 2 deletions

View File

@@ -0,0 +1,128 @@
/**
* Regression tests for skeleton grid CLS (Cumulative Layout Shift) on the
* subscription plan selection page.
*
* FN-346: Verify that the skeleton loading grid and the real plan card grid
* use identical CSS grid classes and the same item count, preventing layout
* shift when the skeleton transitions to real content.
*/
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
// Mock useTranslation (PlanGrid calls t() for plan names, descriptions, badges)
vi.mock("@/lib/i18n", () => ({
useTranslation: () => ({
t: (key: string) => key,
locale: "tr",
setLocale: vi.fn(),
}),
}));
import { PlanGrid, plans } from "@/routes/dashboard/subscription/index";
/**
* Mimics the exact skeleton HTML produced by SubscriptionPage's loading
* state (lines 514-518 of the source file).
*/
function SkeletonPlanGrid() {
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="h-48 w-full rounded-2xl" data-testid="skeleton-item" />
))}
</div>
);
}
describe("Plan grid skeleton CLS regression (FN-346)", () => {
// ─── Grid class string helpers ────────────────────────────────────────────
const EXPECTED_GRID_CLASSES = "grid gap-4 sm:grid-cols-2 lg:grid-cols-4";
function getGridContainer(element: HTMLElement): HTMLElement {
const el = element.querySelector('[class*="grid"]');
if (!el) throw new Error("No grid container found");
return el;
}
// ─── Test 1: Skeleton grid has correct classes and 4 items ────────────────
test("skeleton grid uses correct grid classes and renders 4 placeholders", () => {
render(<SkeletonPlanGrid />);
const grid = getGridContainer(document.body);
expect(grid.className).toBe(EXPECTED_GRID_CLASSES);
const items = screen.getAllByTestId("skeleton-item");
expect(items).toHaveLength(4);
});
// ─── Test 2: PlanGrid uses the same grid classes as skeleton ──────────────
test("PlanGrid grid classes match skeleton grid classes exactly", () => {
render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const grid = getGridContainer(document.body);
expect(grid.className).toBe(EXPECTED_GRID_CLASSES);
// Verify the skeleton grid also uses the same classes
const skeleton = render(<SkeletonPlanGrid />);
const skeletonGrid = getGridContainer(skeleton.container);
expect(skeletonGrid.className).toBe(EXPECTED_GRID_CLASSES);
// Both must be identical
expect(grid.className).toBe(skeletonGrid.className);
});
// ─── Test 3: PlanGrid renders the same number of items as plans.length ────
test("PlanGrid renders exactly plans.length plan cards", () => {
render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const grid = getGridContainer(document.body);
// PlanGrid renders a <button> for each plan
const planButtons = grid.querySelectorAll("button");
expect(planButtons).toHaveLength(plans.length);
// Verify there are exactly 4 plans (the skeleton also has 4 placeholders)
expect(plans).toHaveLength(4);
});
// ─── Test 4: Skeleton placeholder count equals real plan count ────────────
test("skeleton placeholder count (4) equals real plan count", () => {
expect(4).toBe(plans.length);
});
// ─── Test 5: PlanGrid renders all plan names ──────────────────────────────
test("PlanGrid renders all expected plan key names", () => {
render(
<PlanGrid
billingPeriod="yearly"
selectedPlanKey="brand1"
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
// Each plan's name should appear (mocked t() returns the key itself)
for (const plan of plans) {
const nameKey = `subscription.plans.${plan.key}.name`;
expect(screen.getByText(nameKey)).toBeInTheDocument();
}
});
});

View File

@@ -0,0 +1,141 @@
/**
* Integration-level regression test: renders the skeleton grid and the real
* PlanGrid side-by-side and asserts structural parity (grid class + child count).
*
* This test catches drift between the inline skeleton in SubscriptionPage
* and the PlanGrid component. If someone changes one without updating the
* other, this test breaks.
*/
import { render } from "@testing-library/react";
import { vi } from "vitest";
vi.mock("@/lib/i18n", () => ({
useTranslation: () => ({
t: (key: string) => key,
locale: "tr",
setLocale: vi.fn(),
}),
}));
import { PlanGrid, plans } from "@/routes/dashboard/subscription/index";
/** Exact HTML used in SubscriptionPage loading state (lines 514-518). */
const SKELETON_GRID_CLASS = "grid gap-4 sm:grid-cols-2 lg:grid-cols-4";
const SKELETON_PLACEHOLDER_COUNT = 4;
function SkeletonGrid() {
return (
<div className={SKELETON_GRID_CLASS}>
<div className="h-48 w-full rounded-2xl" data-testid="skeleton-placeholder" />
<div className="h-48 w-full rounded-2xl" data-testid="skeleton-placeholder" />
<div className="h-48 w-full rounded-2xl" data-testid="skeleton-placeholder" />
<div className="h-48 w-full rounded-2xl" data-testid="skeleton-placeholder" />
</div>
);
}
describe("Skeleton ↔ PlanGrid structural parity (FN-346)", () => {
test("skeleton grid and PlanGrid grid use identical CSS class strings", () => {
const skeleton = render(<SkeletonGrid />);
const planGrid = render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const skeletonDiv = skeleton.container.querySelector('[class*="grid"]');
const planGridDiv = planGrid.container.querySelector('[class*="grid"]');
expect(skeletonDiv).not.toBeNull();
expect(planGridDiv).not.toBeNull();
expect(skeletonDiv!.className).toBe(planGridDiv!.className);
expect(skeletonDiv!.className).toBe(SKELETON_GRID_CLASS);
});
test("skeleton placeholder count equals PlanGrid rendered plan count", () => {
const skeleton = render(<SkeletonGrid />);
const planGrid = render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const skeletonItems = skeleton.getAllByTestId("skeleton-placeholder");
const planButtons = planGrid.container.querySelectorAll("button");
expect(skeletonItems).toHaveLength(4);
expect(planButtons).toHaveLength(plans.length);
expect(skeletonItems.length).toBe(planButtons.length);
});
test("plans.length is 4, matching skeleton placeholder count", () => {
// If someone adds/removes a plan and doesn't update the skeleton, this
// explicit assertion breaks, forcing them to update the skeleton too.
expect(plans.length).toBe(SKELETON_PLACEHOLDER_COUNT);
});
test("PlanGrid renders correct grid class regardless of props", () => {
// Changing billing period or selection should not change the grid CSS
const variants: Array<{
billingPeriod: "monthly" | "yearly";
selectedPlanKey: string | null;
currentPlanKey: string | null;
activeSubscription: boolean;
}> = [
{
billingPeriod: "monthly",
selectedPlanKey: null,
currentPlanKey: null,
activeSubscription: false,
},
{
billingPeriod: "yearly",
selectedPlanKey: "brand1",
currentPlanKey: null,
activeSubscription: false,
},
{
billingPeriod: "monthly",
selectedPlanKey: "full",
currentPlanKey: "full",
activeSubscription: true,
},
];
for (const props of variants) {
const { container, unmount } = render(<PlanGrid onSelect={vi.fn()} {...props} />);
const gridDiv = container.querySelector('[class*="grid"]');
expect(gridDiv).not.toBeNull();
expect(gridDiv!.className).toBe(SKELETON_GRID_CLASS);
unmount();
}
});
test("skeleton and PlanGrid produce same number of direct grid children", () => {
const skeleton = render(<SkeletonGrid />);
const planGrid = render(
<PlanGrid
billingPeriod="monthly"
selectedPlanKey={null}
currentPlanKey={null}
activeSubscription={false}
onSelect={vi.fn()}
/>,
);
const skeletonDiv = skeleton.container.querySelector('[class*="grid"]');
const planGridDiv = planGrid.container.querySelector('[class*="grid"]');
expect(skeletonDiv!.children.length).toBe(planGridDiv!.children.length);
expect(skeletonDiv!.children.length).toBe(4);
});
});

View File

@@ -105,7 +105,7 @@ interface Subscription {
endDate?: string; endDate?: string;
} }
const plans = [ export const plans = [
{ {
key: "brand1", key: "brand1",
brandLimit: 1, brandLimit: 1,
@@ -986,7 +986,7 @@ function BillingPeriodToggle({
); );
} }
function PlanGrid({ export function PlanGrid({
billingPeriod, billingPeriod,
selectedPlanKey, selectedPlanKey,
currentPlanKey, currentPlanKey,