203 lines
6.5 KiB
TypeScript
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();
|
|
});
|
|
});
|