xtablo-source/apps/main/src/components/SubscriptionCard.test.tsx
2026-04-18 11:09:04 +02:00

203 lines
6.5 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { TestUserStoreProvider, type User } from "../providers/UserStoreProvider";
import { SubscriptionCard } from "./SubscriptionCard";
vi.mock("../hooks/organization", () => ({
useOrganization: vi.fn(),
}));
vi.mock("../hooks/stripe", async (importOriginal) => {
const actual = await importOriginal<typeof import("../hooks/stripe")>();
return {
...actual,
useSubscription: vi.fn(),
useCreateCheckoutSession: () => ({ mutate: vi.fn(), isPending: false }),
useCreatePortalSession: () => ({ mutate: vi.fn(), isPending: false }),
useCancelSubscription: () => ({ mutate: vi.fn(), isPending: false }),
useReactivateSubscription: () => ({ mutate: vi.fn(), isPending: false }),
};
});
vi.mock("../hooks/auth", () => ({
useAuthedApi: () => ({}),
}));
import { useOrganization } from "../hooks/organization";
import { useSubscription } from "../hooks/stripe";
const mockUseOrganization = vi.mocked(useOrganization);
const mockUseSubscription = vi.mocked(useSubscription);
const baseUser: User = {
id: "user-1",
short_user_id: "u1",
name: "Test User",
first_name: "Test",
last_name: "User",
email: "test@example.com",
avatar_url: null,
is_temporary: false,
is_client: false,
client_onboarded_at: null,
last_signed_in: null,
plan: "none",
created_at: new Date().toISOString(),
};
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function renderCard(
user: User,
orgData: ReturnType<typeof useOrganization>["data"],
subscription: ReturnType<typeof useSubscription>["data"] = undefined
) {
mockUseOrganization.mockReturnValue({
data: orgData,
isLoading: false,
error: null,
} as ReturnType<typeof useOrganization>);
mockUseSubscription.mockReturnValue({
data: subscription,
isLoading: false,
error: null,
} as ReturnType<typeof useSubscription>);
return render(
<QueryClientProvider client={queryClient}>
<TestUserStoreProvider user={user}>
<SubscriptionCard />
</TestUserStoreProvider>
</QueryClientProvider>
);
}
const baseOrg = {
organization: {
id: 1,
name: "Org",
plan: "none",
member_count: 1,
tablo_count: 0,
logo_url: null,
},
members: [],
invites_sent: [],
trial_starts_at: "2026-01-01",
trial_ends_at: "2026-02-01",
is_trial_expired: false,
required_plan: "solo" as const,
required_team_quantity: 1,
active_subscription_plan: null,
active_subscription_quantity: 0,
is_billing_owner: true,
};
describe("SubscriptionCard", () => {
it("shows 'Sans abonnement' badge when there is no subscription", () => {
renderCard(baseUser, baseOrg);
expect(screen.getByText("Sans abonnement")).toBeInTheDocument();
});
it("shows Founder badge and unlimited info for annual plan", () => {
const founderOrg = { ...baseOrg, active_subscription_plan: "annual" as const };
const founderUser = { ...baseUser, plan: "standard" as const };
renderCard(founderUser, founderOrg);
expect(screen.getByText("Founder")).toBeInTheDocument();
expect(screen.getByText(/Plan Founder \(annuel\)/)).toBeInTheDocument();
});
it("shows recommended plan for non-paying billing owner", () => {
renderCard(baseUser, baseOrg);
expect(screen.getByText(/Plan recommandé: Solo/)).toBeInTheDocument();
expect(screen.getByText(/Passer au plan Solo/)).toBeInTheDocument();
expect(screen.getByText("Devenir Founder (99€/an)")).toBeInTheDocument();
});
it("shows team as recommended plan when required", () => {
const teamOrg = {
...baseOrg,
required_plan: "team" as const,
required_team_quantity: 3,
};
renderCard(baseUser, teamOrg);
expect(screen.getByText(/Plan recommandé: Teams/)).toBeInTheDocument();
expect(screen.getByText(/3 sièges Teams/)).toBeInTheDocument();
});
it("shows billing owner restriction when user is not billing owner", () => {
const nonOwnerOrg = { ...baseOrg, is_billing_owner: false };
renderCard(baseUser, nonOwnerOrg);
expect(screen.getByText(/Seul le propriétaire de facturation/)).toBeInTheDocument();
expect(screen.queryByText(/Passer au plan/)).not.toBeInTheDocument();
});
it("shows active subscription details with manage and cancel buttons", () => {
const teamOrg = {
...baseOrg,
active_subscription_plan: "team" as const,
active_subscription_quantity: 3,
};
const activeSubscription = {
id: "sub_1",
status: "active",
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 30,
cancel_at_period_end: false,
};
renderCard(baseUser, teamOrg, activeSubscription as any);
expect(screen.getByText("Actif")).toBeInTheDocument();
expect(screen.getByText("Gérer l'abonnement")).toBeInTheDocument();
expect(screen.getByText("Annuler")).toBeInTheDocument();
});
it("shows trialing badge for trialing subscription", () => {
const teamOrg = {
...baseOrg,
active_subscription_plan: "team" as const,
};
const trialingSubscription = {
id: "sub_1",
status: "trialing",
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 14,
cancel_at_period_end: false,
};
renderCard(baseUser, teamOrg, trialingSubscription as any);
expect(screen.getByText("Période d'essai")).toBeInTheDocument();
});
it("shows past_due badge for overdue subscription", () => {
const teamOrg = {
...baseOrg,
active_subscription_plan: "team" as const,
};
const pastDueSubscription = {
id: "sub_1",
status: "past_due",
current_period_end: Math.floor(Date.now() / 1000) - 86400,
cancel_at_period_end: false,
};
renderCard(baseUser, teamOrg, pastDueSubscription as any);
expect(screen.getByText("Paiement en retard")).toBeInTheDocument();
});
it("shows reactivation UI for canceled subscription", () => {
const teamOrg = {
...baseOrg,
active_subscription_plan: "team" as const,
active_subscription_quantity: 2,
};
const canceledSubscription = {
id: "sub_1",
status: "active",
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 15,
cancel_at_period_end: true,
};
renderCard(baseUser, teamOrg, canceledSubscription as any);
expect(screen.getByText(/Abonnement en cours d'annulation/)).toBeInTheDocument();
expect(screen.getByText("Réactiver l'abonnement")).toBeInTheDocument();
});
});