- useCustomerPortal hook (mutation + redirect full-page) - AccountBillingSection: badge plan + bouton Gérer mon abonnement (Standard/Premium) - ParametresPage: page conteneur /parametres avec section billing - PricingPage: Standard→Premium redirige vers Customer Portal (prorata natif Stripe) - Tests: 212 → 219 verts (+7) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||
import { renderHook, act, waitFor } from '@testing-library/react'
|
||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||
import type { ReactNode } from 'react'
|
||
|
||
// ─── Mocks ───────────────────────────────────────────────────────────────────
|
||
|
||
const { createCustomerPortalSessionMock } = vi.hoisted(() => ({
|
||
createCustomerPortalSessionMock: vi.fn(),
|
||
}))
|
||
|
||
vi.mock('../api', () => ({
|
||
createCustomerPortalSession: createCustomerPortalSessionMock,
|
||
}))
|
||
|
||
import { useCustomerPortal } from '../hooks/useCustomerPortal'
|
||
|
||
afterEach(() => {
|
||
createCustomerPortalSessionMock.mockReset()
|
||
})
|
||
|
||
function wrapper({ children }: { children: ReactNode }) {
|
||
const queryClient = new QueryClient({
|
||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||
})
|
||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||
}
|
||
|
||
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||
|
||
describe('useCustomerPortal', () => {
|
||
it("openPortal() succès → window.location.href set sur l'URL portal", async () => {
|
||
const originalLocation = window.location
|
||
const hrefSetter = vi.fn()
|
||
Object.defineProperty(window, 'location', {
|
||
configurable: true,
|
||
writable: true,
|
||
value: {
|
||
...originalLocation,
|
||
get href() {
|
||
return originalLocation.href
|
||
},
|
||
set href(v: string) {
|
||
hrefSetter(v)
|
||
},
|
||
},
|
||
})
|
||
|
||
createCustomerPortalSessionMock.mockResolvedValue({
|
||
url: 'https://billing.stripe.com/p/session/abc',
|
||
})
|
||
|
||
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
|
||
act(() => {
|
||
result.current.openPortal()
|
||
})
|
||
|
||
await waitFor(() => {
|
||
expect(hrefSetter).toHaveBeenCalledWith('https://billing.stripe.com/p/session/abc')
|
||
})
|
||
|
||
Object.defineProperty(window, 'location', {
|
||
configurable: true,
|
||
writable: true,
|
||
value: originalLocation,
|
||
})
|
||
})
|
||
|
||
it('erreur backend → message backend propagé dans `error`', async () => {
|
||
createCustomerPortalSessionMock.mockRejectedValue(
|
||
new Error('Aucun abonnement actif trouvé. Souscrivez d’abord à un plan.'),
|
||
)
|
||
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
|
||
|
||
act(() => {
|
||
result.current.openPortal()
|
||
})
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.error).toMatch(/Aucun abonnement actif trouvé/)
|
||
})
|
||
})
|
||
|
||
it('isLoading vrai pendant la mutation pending', async () => {
|
||
createCustomerPortalSessionMock.mockReturnValue(new Promise(() => {}))
|
||
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
|
||
|
||
expect(result.current.isLoading).toBe(false)
|
||
act(() => {
|
||
result.current.openPortal()
|
||
})
|
||
|
||
await waitFor(() => {
|
||
expect(result.current.isLoading).toBe(true)
|
||
})
|
||
})
|
||
})
|