From 169de5244e863dc14e51424a1c81f9baece8b85c Mon Sep 17 00:00:00 2001 From: Suejung Shin Date: Mon, 13 Jan 2025 23:51:35 -0800 Subject: [PATCH] fix tests --- src/pages/PlanPage/PlanPage.test.jsx | 28 +- .../BillingDetails/BillingDetails.test.tsx | 16 +- .../BillingDetails/BillingDetails.tsx | 2 +- .../Address/AddressForm.test.tsx | 240 ++++++++++++++++ .../EditPaymentMethods.test.tsx | 81 ++++++ .../EditPaymentMethods/EditPaymentMethods.tsx | 23 +- .../PaymentMethod/PaymentMethodForm.test.tsx | 218 ++++++++++++++ .../PaymentMethod/PaymentMethodForm.tsx | 9 +- .../Address/AddressCard.test.tsx | 266 +++--------------- .../PaymentMethod/PaymentMethod.test.tsx | 243 ++++------------ .../PaymentMethod/PaymentMethod.tsx | 1 + 11 files changed, 675 insertions(+), 452 deletions(-) create mode 100644 src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.test.tsx create mode 100644 src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.test.tsx create mode 100644 src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.test.tsx diff --git a/src/pages/PlanPage/PlanPage.test.jsx b/src/pages/PlanPage/PlanPage.test.jsx index 3cdb714f09..b7bbc12378 100644 --- a/src/pages/PlanPage/PlanPage.test.jsx +++ b/src/pages/PlanPage/PlanPage.test.jsx @@ -11,6 +11,8 @@ import { MemoryRouter, Route } from 'react-router-dom' import config from 'config' +import { ThemeContextProvider } from 'shared/ThemeContext' + import PlanPage from './PlanPage' vi.mock('config') @@ -44,18 +46,20 @@ const wrapper = ({ children }) => ( - - - {children} - { - testLocation = location - return null - }} - /> - - + + + + {children} + { + testLocation = location + return null + }} + /> + + + ) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.test.tsx index c524ccd390..47536948d5 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.test.tsx @@ -8,11 +8,15 @@ import { Plans } from 'shared/utils/billing' import BillingDetails from './BillingDetails' -vi.mock('./PaymentCard/PaymentCard', () => ({ default: () => 'Payment Card' })) +vi.mock('./ViewPaymentMethod/PaymentMethod/PaymentMethod', () => ({ + default: () => 'Payment Method', +})) vi.mock('./EmailAddress/EmailAddress', () => ({ default: () => 'Email Address', })) -vi.mock('./Address/AddressCard', () => ({ default: () => 'Address Card' })) +vi.mock('./ViewPaymentMethod/Address/AddressCard', () => ({ + default: () => 'Address Card', +})) const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, @@ -84,12 +88,12 @@ describe('BillingDetails', () => { } describe('when there is a subscription', () => { - it('renders the payment card', async () => { + it('renders the payment method card', async () => { setup({ hasSubscription: true }) render(, { wrapper }) - const paymentCard = await screen.findByText(/Payment Card/) - expect(paymentCard).toBeInTheDocument() + const paymentCards = await screen.findAllByText(/Payment Method/) + expect(paymentCards.length).toBeGreaterThan(0) }) it('renders the email address component', async () => { @@ -132,7 +136,7 @@ describe('BillingDetails', () => { it('renders the payment card', async () => { render(, { wrapper }) - const paymentCard = screen.queryByText(/Payment Card/) + const paymentCard = screen.queryByText(/Payment Method/) expect(paymentCard).not.toBeInTheDocument() }) }) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx index b9874d0dd4..bc74d38261 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx @@ -71,7 +71,7 @@ function BillingDetails() { setEditMode={setEditMode} provider={provider} owner={owner} - existingSubscriptionDetail={subscriptionDetail} + subscriptionDetail={subscriptionDetail} /> ) : ( <> diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.test.tsx new file mode 100644 index 0000000000..8f6b836972 --- /dev/null +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.test.tsx @@ -0,0 +1,240 @@ +import { Elements } from '@stripe/react-stripe-js' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter, Route } from 'react-router-dom' +import { vi } from 'vitest' +import { z } from 'zod' + +import { SubscriptionDetailSchema } from 'services/account/useAccountDetails' + +import AddressForm from './AddressForm' + + +const queryClient = new QueryClient() + +const mockGetElement = vi.fn() +const mockGetValue = vi.fn() + +vi.mock('@stripe/react-stripe-js', async () => { + const actual = await vi.importActual('@stripe/react-stripe-js') + return { + ...actual, + useElements: () => ({ + getElement: mockGetElement.mockReturnValue({ + getValue: mockGetValue.mockResolvedValue({ + complete: true, + value: { + name: 'John Doe', + address: { + line1: '123 Main St', + line2: null, + city: 'San Francisco', + state: 'CA', + postal_code: '94105', + country: 'US', + }, + }, + }), + }), + }), + } +}) + +const wrapper: React.FC = ({ children }) => ( + + + + {children} + + + +) + +const mockSubscriptionDetail: z.infer = { + defaultPaymentMethod: { + billingDetails: { + address: { + line1: '123 Main St', + city: 'San Francisco', + state: 'CA', + postalCode: '94105', + country: 'US', + line2: null, + }, + phone: '1234567890', + name: 'John Doe', + email: 'test@example.com', + }, + card: { + brand: 'visa', + expMonth: 12, + expYear: 2025, + last4: '4242', + }, + }, + currentPeriodEnd: 1706851492, + cancelAtPeriodEnd: false, + customer: { + id: 'cust_123', + email: 'test@example.com', + }, + latestInvoice: null, + taxIds: [], + trialEnd: null, +} + +const mocks = { + useUpdateBillingAddress: vi.fn(), +} + +vi.mock('src/services/account/useUpdateBillingAddress', () => ({ + useUpdateBillingAddress: () => mocks.useUpdateBillingAddress(), +})) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('AddressForm', () => { + const setup = () => { + const mutation = vi.fn() + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: mutation, + isLoading: false, + }) + return { user: userEvent.setup() } + } + + it('renders the form', () => { + render( + {}} + />, + { wrapper } + ) + + expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument() + }) + + describe('when submitting', () => { + it.only('calls the service to update the address', async () => { + const { user } = setup() + const updateAddress = vi.fn() + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: updateAddress, + isLoading: false, + }) + + render( + {}} + />, + { wrapper } + ) + + await user.click(screen.getByRole('button', { name: /save/i })) + + expect(updateAddress).toHaveBeenCalled() + }) + }) + + describe('when the user clicks on cancel', () => { + it('calls the closeForm prop', async () => { + const { user } = setup() + const closeForm = vi.fn() + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: vi.fn(), + isLoading: false, + }) + + render( + , + { wrapper } + ) + + await user.click(screen.getByRole('button', { name: /cancel/i })) + + expect(closeForm).toHaveBeenCalled() + }) + }) + + describe('when there is an error in the form', () => { + it('renders the error', async () => { + const randomError = 'not a valid address' + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: vi.fn(), + error: randomError, + }) + + render( + {}} + />, + { wrapper } + ) + + expect(screen.getByText(randomError)).toBeInTheDocument() + }) + }) + + describe('when the form is loading', () => { + it('has the save and cancel buttons disabled', () => { + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: vi.fn(), + isLoading: true, + }) + + render( + {}} + />, + { wrapper } + ) + + expect(screen.getByRole('button', { name: /save/i })).toBeDisabled() + expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled() + }) + }) +}) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.test.tsx new file mode 100644 index 0000000000..9cf7e4cba8 --- /dev/null +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.test.tsx @@ -0,0 +1,81 @@ +import { Elements } from '@stripe/react-stripe-js' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route } from 'react-router-dom' +import { z } from 'zod' + +import { SubscriptionDetailSchema } from 'services/account' + +import EditPaymentMethods from './EditPaymentMethods' + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) + +vi.mock('./PaymentMethod/PaymentMethodForm', () => ({ + default: () => 'Payment Method Form', +})) + +vi.mock('./Address/AddressForm', () => ({ + default: () => 'Address Form', +})) + +const wrapper: React.FC = ({ children }) => ( + + + + {children} + + + +) + +const mockSubscriptionDetail: z.infer = { + defaultPaymentMethod: { + billingDetails: { + address: { + line1: '123 Main St', + city: 'San Francisco', + state: 'CA', + postalCode: '94105', + country: 'US', + line2: null, + }, + phone: '1234567890', + name: 'John Doe', + email: 'test@example.com', + }, + card: { + brand: 'visa', + expMonth: 12, + expYear: 2025, + last4: '4242', + }, + }, + currentPeriodEnd: 1706851492, + cancelAtPeriodEnd: false, + customer: { + id: 'cust_123', + email: 'test@example.com', + }, + latestInvoice: null, + taxIds: [], + trialEnd: null, +} + +describe('EditPaymentMethod', () => { + it('renders the expected forms', () => { + render( + {}} + provider="gh" + owner="codecov" + subscriptionDetail={mockSubscriptionDetail} + />, + { wrapper } + ) + + expect(screen.getByText(/Payment Method Form/)).toBeInTheDocument() + expect(screen.getByText(/Address Form/)).toBeInTheDocument() + }) +}) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.tsx index 3b135b829a..1d0539bdd2 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/EditPaymentMethods.tsx @@ -8,19 +8,19 @@ import PaymentMethodForm from './PaymentMethod/PaymentMethodForm' import { SECONDARY_PAYMENT_FEATURE_ENABLED } from '../BillingDetails' -interface EditPaymentMethodProps { +interface EditPaymentMethodsProps { setEditMode: (isEditMode: boolean) => void provider: string owner: string - existingSubscriptionDetail: z.infer + subscriptionDetail: z.infer } -const EditPaymentMethod = ({ +const EditPaymentMethods = ({ setEditMode, provider, owner, - existingSubscriptionDetail, -}: EditPaymentMethodProps) => { + subscriptionDetail, +}: EditPaymentMethodsProps) => { const [activeTab, setActiveTab] = useState<'primary' | 'secondary'>('primary') const secondaryPaymentMethodFeatureEnabled = SECONDARY_PAYMENT_FEATURE_ENABLED @@ -57,16 +57,15 @@ const EditPaymentMethod = ({ closeForm={() => setEditMode(false)} provider={provider} owner={owner} - existingSubscriptionDetail={existingSubscriptionDetail} + subscriptionDetail={subscriptionDetail} /> setEditMode(false)} provider={provider} @@ -80,7 +79,7 @@ const EditPaymentMethod = ({ closeForm={() => setEditMode(false)} provider={provider} owner={owner} - existingSubscriptionDetail={existingSubscriptionDetail} + subscriptionDetail={subscriptionDetail} /> setEditMode(false)} @@ -95,4 +94,4 @@ const EditPaymentMethod = ({ ) } -export default EditPaymentMethod +export default EditPaymentMethods diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.test.tsx new file mode 100644 index 0000000000..e77927c042 --- /dev/null +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.test.tsx @@ -0,0 +1,218 @@ +import { Elements } from '@stripe/react-stripe-js' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter, Route } from 'react-router-dom' +import { vi } from 'vitest' +import { z } from 'zod' + +import { SubscriptionDetailSchema } from 'services/account/useAccountDetails' + +import PaymentMethodForm from './PaymentMethodForm' + +const queryClient = new QueryClient() + +const mockElements = { + submit: vi.fn(), + getElement: vi.fn(), +} + +vi.mock('@stripe/react-stripe-js', () => ({ + Elements: ({ children }: { children: React.ReactNode }) => children, + useElements: () => mockElements, + PaymentElement: 'div', +})) + +const wrapper: React.FC = ({ children }) => ( + + + + {children} + + + +) + +const subscriptionDetail: z.infer = { + defaultPaymentMethod: { + billingDetails: { + address: { + line1: '123 Main St', + city: 'San Francisco', + state: 'CA', + postalCode: '94105', + country: 'US', + line2: null, + }, + phone: '1234567890', + name: 'John Doe', + email: 'test@example.com', + }, + card: { + brand: 'visa', + expMonth: 12, + expYear: 2025, + last4: '4242', + }, + }, + currentPeriodEnd: 1706851492, + cancelAtPeriodEnd: false, + customer: { + id: 'cust_123', + email: 'test@example.com', + }, + latestInvoice: null, + taxIds: [], + trialEnd: null, +} + +const mocks = { + useUpdatePaymentMethod: vi.fn(), +} + +vi.mock('services/account/useUpdatePaymentMethod', () => ({ + useUpdatePaymentMethod: () => mocks.useUpdatePaymentMethod(), +})) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('PaymentMethodForm', () => { + describe('when the user clicks on Edit payment method', () => { + it(`doesn't render the payment method anymore`, async () => { + const user = userEvent.setup() + const updatePaymentMethod = vi.fn() + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: updatePaymentMethod, + isLoading: false, + }) + + render( + {}} + />, + { wrapper } + ) + await user.click(screen.getByTestId('update-payment-method')) + + expect(screen.queryByText(/Visa/)).not.toBeInTheDocument() + }) + + it('renders the form', async () => { + const user = userEvent.setup() + const updatePaymentMethod = vi.fn() + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: updatePaymentMethod, + isLoading: false, + }) + render( + {}} + />, + { wrapper } + ) + await user.click(screen.getByTestId('update-payment-method')) + + expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument() + }) + + describe('when submitting', () => { + it('calls the service to update the payment method', async () => { + const user = userEvent.setup() + const updatePaymentMethod = vi.fn() + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: updatePaymentMethod, + isLoading: false, + }) + render( + {}} + />, + { wrapper } + ) + await user.click(screen.getByTestId('update-payment-method')) + expect(updatePaymentMethod).toHaveBeenCalled() + }) + }) + + describe('when the user clicks on cancel', () => { + it(`doesn't render the form anymore`, async () => { + const user = userEvent.setup() + const closeForm = vi.fn() + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: vi.fn(), + isLoading: false, + }) + render( + , + { wrapper } + ) + + await user.click(screen.getByTestId('update-payment-method')) + await user.click(screen.getByRole('button', { name: /Cancel/ })) + + expect(closeForm).toHaveBeenCalled() + }) + }) + }) + + describe('when there is an error in the form', () => { + it('renders the error', async () => { + const user = userEvent.setup() + const randomError = 'not rich enough' + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: vi.fn(), + error: { message: randomError }, + }) + render( + {}} + />, + { wrapper } + ) + + await user.click(screen.getByTestId('update-payment-method')) + + expect(screen.getByText(randomError)).toBeInTheDocument() + }) + }) + + describe('when the form is loading', () => { + it('has the error and save button disabled', async () => { + mocks.useUpdatePaymentMethod.mockReturnValue({ + mutate: vi.fn(), + isLoading: true, + }) + render( + {}} + />, + { wrapper } + ) + + expect(screen.queryByRole('button', { name: /Save/i })).toBeDisabled() + expect(screen.queryByRole('button', { name: /Cancel/i })).toBeDisabled() + }) + }) +}) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.tsx index 46ac35a50d..08ef6a0992 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/PaymentMethod/PaymentMethodForm.tsx @@ -11,14 +11,14 @@ interface PaymentMethodFormProps { closeForm: () => void provider: string owner: string - existingSubscriptionDetail: z.infer + subscriptionDetail: z.infer } const PaymentMethodForm = ({ closeForm, provider, owner, - existingSubscriptionDetail, + subscriptionDetail, }: PaymentMethodFormProps) => { const [errorState, _] = useState('') const elements = useElements() @@ -31,8 +31,7 @@ const PaymentMethodForm = ({ } = useUpdatePaymentMethod({ provider, owner, - email: - existingSubscriptionDetail?.defaultPaymentMethod?.billingDetails?.email, + email: subscriptionDetail?.defaultPaymentMethod?.billingDetails?.email, }) async function submit(e: React.FormEvent) { @@ -75,7 +74,7 @@ const PaymentMethodForm = ({