whisper-money/resources/js/pages/settings/billing.test.tsx

130 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PricingConfig } from '@/types/pricing';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import Billing from './billing';
const mocks = vi.hoisted(() => ({
axiosPost: vi.fn(() => Promise.resolve({ data: {} })),
axiosDelete: vi.fn(() => Promise.resolve({ data: {} })),
state: { hasProPlan: false, hasAiConsent: false },
}));
vi.mock('axios', () => ({
default: {
post: mocks.axiosPost,
delete: mocks.axiosDelete,
get: vi.fn(),
isAxiosError: () => false,
},
}));
const pricing: PricingConfig = {
plans: {
monthly: {
name: 'Monthly',
price: 3.99,
original_price: null,
stripe_lookup_key: 'monthly',
billing_period: 'month',
features: [],
},
yearly: {
name: 'Annual',
price: 23.88,
original_price: 47.88,
stripe_lookup_key: 'yearly',
billing_period: 'year',
features: [],
},
},
defaultPlan: 'yearly',
bestValuePlan: 'yearly',
promo: { enabled: false, code: '', description: '', badge: '' },
currency: 'EUR',
};
vi.mock('@inertiajs/react', () => ({
Head: ({ title }: { title: string }) => <title>{title}</title>,
router: { post: vi.fn(), visit: vi.fn() },
usePage: () => ({
props: {
auth: {
isDemoAccount: false,
hasProPlan: mocks.state.hasProPlan,
},
pricing,
locale: 'en',
hasAiConsent: mocks.state.hasAiConsent,
},
}),
}));
vi.mock('@/layouts/app-layout', () => ({
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('@/layouts/settings/layout', () => ({
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('sonner', () => ({
toast: { success: vi.fn(), error: vi.fn() },
}));
function aiCheckbox(): HTMLElement {
return screen.getByRole('checkbox', { name: /Allow AI categorization/i });
}
describe('Billing AI categorization toggle', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.state.hasProPlan = false;
mocks.state.hasAiConsent = false;
});
it('prompts a free user to subscribe instead of recording consent', async () => {
render(<Billing />);
// Capture the element up front: once the dialog opens, Radix marks the
// background as aria-hidden, so getByRole can no longer see it.
const checkbox = aiCheckbox();
fireEvent.click(checkbox);
expect(
await screen.findByText('AI categorization is a paid feature'),
).toBeInTheDocument();
// The consent must NOT be recorded — that is what would lock the free
// user behind the paywall on the next navigation.
expect(mocks.axiosPost).not.toHaveBeenCalled();
expect(checkbox).not.toBeChecked();
});
it('records consent directly for a subscribed user', async () => {
mocks.state.hasProPlan = true;
render(<Billing />);
fireEvent.click(aiCheckbox());
await waitFor(() => expect(mocks.axiosPost).toHaveBeenCalledTimes(1));
expect(
screen.queryByText('AI categorization is a paid feature'),
).not.toBeInTheDocument();
});
it('lets a free user with existing consent revoke it (escape the lock)', async () => {
mocks.state.hasAiConsent = true;
render(<Billing />);
// Unchecking is not gated — it must revoke so a previously locked free
// user can get out from behind the paywall.
fireEvent.click(aiCheckbox());
await waitFor(() => expect(mocks.axiosDelete).toHaveBeenCalledTimes(1));
expect(mocks.axiosPost).not.toHaveBeenCalled();
expect(
screen.queryByText('AI categorization is a paid feature'),
).not.toBeInTheDocument();
});
});