From 2ade2fc648f8c93bf7a02aceb0ddc62dbc2995db 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 | 18 +- .../BillingDetails/BillingDetails.tsx | 2 +- .../Address/AddressForm.test.tsx | 232 ++++++++++ .../Address/AddressForm.tsx | 14 +- .../EditPaymentMethods.test.tsx | 81 ++++ .../EditPaymentMethods/EditPaymentMethods.tsx | 24 +- .../PaymentMethod/PaymentMethodForm.test.tsx | 218 ++++++++++ .../PaymentMethod/PaymentMethodForm.tsx | 9 +- .../Address/Address.test.tsx | 215 +++++++++ .../Address/{AddressCard.tsx => Address.tsx} | 83 ++-- .../Address/AddressCard.test.tsx | 409 ------------------ .../PaymentMethod/PaymentMethod.test.tsx | 243 +++-------- .../PaymentMethod/PaymentMethod.tsx | 1 + .../ViewPaymentMethod/ViewPaymentMethod.tsx | 4 +- .../account/useUpdatePaymentMethod.ts | 2 +- src/shared/ThemeContext/ThemeContext.tsx | 4 +- 17 files changed, 886 insertions(+), 701 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 create mode 100644 src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/Address.test.tsx rename src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/{AddressCard.tsx => Address.tsx} (57%) delete mode 100644 src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/AddressCard.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..c8d8bfead7 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/Address', () => ({ + default: () => 'Address', +})) 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 () => { @@ -104,7 +108,7 @@ describe('BillingDetails', () => { setup({ hasSubscription: true }) render(, { wrapper }) - const addressCard = await screen.findByText(/Address Card/) + const addressCard = await screen.findByText(/Address/) expect(addressCard).toBeInTheDocument() }) @@ -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..3c547763a3 --- /dev/null +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.test.tsx @@ -0,0 +1,232 @@ +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('services/account/useUpdateBillingAddress', () => ({ + useUpdateBillingAddress: () => mocks.useUpdateBillingAddress(), +})) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('AddressForm', () => { + const setup = () => { + return { user: userEvent.setup() } + } + + it('renders the form', () => { + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: vi.fn(), + isLoading: false, + }) + + render( + {}} + />, + { wrapper } + ) + + expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument() + }) + + describe('when submitting', () => { + it('calls the service to update the address', async () => { + const user = userEvent.setup() + const updateAddress = vi.fn() + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: updateAddress, + isLoading: false, + }) + + render( + {}} + />, + { wrapper } + ) + + await user.click(screen.getByTestId('submit-address-update')) + expect(updateAddress).toHaveBeenCalledWith( + { + name: 'John Doe', + address: { + line1: '123 Main St', + line2: null, + city: 'San Francisco', + state: 'CA', + postal_code: '94105', + country: 'US', + }, + }, + expect.any(Object) + ) + }) + }) + + 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 the form is loading', () => { + it('has the save and cancel buttons disabled', () => { + mocks.useUpdateBillingAddress.mockReturnValue({ + mutate: vi.fn(), + isLoading: true, + error: null, + reset: vi.fn(), + }) + + 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/Address/AddressForm.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.tsx index ba3885707b..027ba1c985 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethods/Address/AddressForm.tsx @@ -8,7 +8,7 @@ import Button from 'ui/Button' interface AddressFormProps { address?: z.infer - name?: string | null | undefined + name?: string closeForm: () => void provider: string owner: string @@ -23,12 +23,7 @@ function AddressForm({ }: AddressFormProps) { const elements = useElements() - const { - mutate: updateAddress, - isLoading, - error, - reset, - } = useUpdateBillingAddress({ + const { mutate: updateAddress, isLoading } = useUpdateBillingAddress({ provider, owner, }) @@ -47,8 +42,6 @@ function AddressForm({ } } - const showError = error && !reset - return (
@@ -72,7 +65,6 @@ function AddressForm({ }, }} /> -

{showError && error}

@@ -90,7 +81,6 @@ function AddressForm({ variant="plain" disabled={isLoading} onClick={closeForm} - to={undefined} > Cancel 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..ee029ca825 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,16 @@ const EditPaymentMethod = ({ closeForm={() => setEditMode(false)} provider={provider} owner={owner} - existingSubscriptionDetail={existingSubscriptionDetail} + subscriptionDetail={subscriptionDetail} /> setEditMode(false)} provider={provider} @@ -80,7 +80,7 @@ const EditPaymentMethod = ({ closeForm={() => setEditMode(false)} provider={provider} owner={owner} - existingSubscriptionDetail={existingSubscriptionDetail} + subscriptionDetail={subscriptionDetail} /> setEditMode(false)} @@ -95,4 +95,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..2deb95111f 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() @@ -32,7 +32,8 @@ const PaymentMethodForm = ({ provider, owner, email: - existingSubscriptionDetail?.defaultPaymentMethod?.billingDetails?.email, + subscriptionDetail?.defaultPaymentMethod?.billingDetails?.email || + undefined, }) async function submit(e: React.FormEvent) { @@ -75,7 +76,7 @@ const PaymentMethodForm = ({

+
+
+ + ) + } + + return ( +

Billing address

{isAddressSameAsPrimary ? ( @@ -83,28 +78,8 @@ function BillingInner({ )}
- ) - } - - return ( -
-

- No address has been set. Please contact support if you think it’s an - error or set it yourself. -

-
- -
) } -export default AddressCard +export default Address diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/AddressCard.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/AddressCard.test.tsx deleted file mode 100644 index 49ca52c423..0000000000 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/Address/AddressCard.test.tsx +++ /dev/null @@ -1,409 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { z } from 'zod' - -import { SubscriptionDetailSchema } from 'services/account' -import { ThemeContextProvider } from 'shared/ThemeContext' - -import AddressCard from './AddressCard' - -const mocks = vi.hoisted(() => ({ - useUpdateBillingAddress: vi.fn(), -})) - -vi.mock('services/account/useUpdateBillingAddress', async () => { - const actual = await import('services/account/useUpdateBillingAddress') - return { - ...actual, - useUpdateBillingAddress: mocks.useUpdateBillingAddress, - } -}) - -const subscriptionDetail = { - defaultPaymentMethod: { - card: { - brand: 'visa', - expMonth: 12, - expYear: 2021, - last4: '1234', - }, - billingDetails: { - name: 'Bob Smith', - address: { - line1: '123 Sesame St.', - line2: 'Apt A', - city: 'San Francisco', - country: 'US', - state: 'CA', - postalCode: '12345', - }, - }, - }, - currentPeriodEnd: 1606851492, - cancelAtPeriodEnd: false, -} as z.infer - -const wrapper: React.FC = ({ children }) => ( - {children} -) - -// mocking stripe components -vi.mock('@stripe/react-stripe-js', () => { - function makeFakeComponent(name: string) { - // mocking onReady to be called after a bit of time - return function Component({ _onReady }: { _onReady?: any }) { - return name - } - } - - return { - useElements: () => ({ - getElement: vi.fn().mockReturnValue({ - getValue: vi.fn().mockResolvedValue({ - complete: true, - value: { - address: {}, - }, - }), - }), - update: vi.fn(), - }), - useStripe: () => ({}), - AddressElement: makeFakeComponent('AddressElement'), - } -}) - -describe('AddressCard', () => { - function setup() { - const user = userEvent.setup() - - return { user } - } - - describe(`when the user doesn't have any subscriptionDetail`, () => { - // NOTE: This test is misleading because we hide this component from a higher level in - // BillingDetails.tsx if there is no subscriptionDetail - it('renders the set card message', () => { - render( - {}} - />, - { wrapper } - ) - - expect( - screen.getByText( - /No address has been set. Please contact support if you think it's an error or set it yourself./ - ) - ).toBeInTheDocument() - }) - }) - - describe(`when the user doesn't have billing details`, () => { - it('renders an error message', () => { - render( - {}} - />, - { wrapper } - ) - - expect( - screen.getByText( - /No address has been set. Please contact support if you think it's an error or set it yourself./ - ) - ).toBeInTheDocument() - }) - - describe('when the user clicks on "Set Address"', () => { - it(`doesn't render address info stuff anymore`, async () => { - const { user } = setup() - render( - {}} - />, - { wrapper } - ) - - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: () => null, - isLoading: false, - }) - await user.click(screen.getByTestId('open-modal')) - - expect( - screen.queryByText(/123 Sesame St. Apt A/) - ).not.toBeInTheDocument() - }) - - it('renders the address form component', async () => { - const { user } = setup() - render( - {}} - />, - { wrapper } - ) - - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: () => null, - isLoading: false, - }) - await user.click(screen.getByTestId('open-modal')) - - expect( - screen.getByRole('button', { name: /update/i }) - ).toBeInTheDocument() - }) - }) - }) - - describe('when the user has an address', () => { - it('renders the address information', () => { - render( - {}} - />, - { wrapper } - ) - - expect(screen.getByText(/Billing address/)).toBeInTheDocument() - expect(screen.getByText(/123 Sesame St. Apt A/)).toBeInTheDocument() - expect(screen.getByText(/San Francisco, CA 12345/)).toBeInTheDocument() - }) - - it('can render partial information too', () => { - render( - {}} - />, - { wrapper } - ) - - expect(screen.getByText('Cardholder name')).toBeInTheDocument() - expect(screen.getByText('N/A')).toBeInTheDocument() - expect(screen.getByText('Billing address')).toBeInTheDocument() - expect(screen.queryByText(/null/)).not.toBeInTheDocument() - expect(screen.getByText('12345')).toBeInTheDocument() - }) - - it('renders the card holder information', () => { - render( - {}} - />, - { wrapper } - ) - - expect(screen.getByText(/Cardholder name/)).toBeInTheDocument() - expect(screen.getByText(/Bob Smith/)).toBeInTheDocument() - }) - }) - - describe('when the user clicks on Edit', () => { - it(`doesn't render the card anymore`, async () => { - const { user } = setup() - const updateAddress = vi.fn() - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: updateAddress, - isLoading: false, - }) - - render( - {}} - />, - { wrapper } - ) - await user.click(screen.getByTestId('edit-address')) - - expect(screen.queryByText(/Cardholder name/)).not.toBeInTheDocument() - expect(screen.queryByText(/Bob Smith/)).not.toBeInTheDocument() - expect(screen.queryByText(/Billing address/)).not.toBeInTheDocument() - expect(screen.queryByText(/123 Sesame St. Apt A/)).not.toBeInTheDocument() - expect( - screen.queryByText(/San Francisco, CA 12345/) - ).not.toBeInTheDocument() - }) - - it('renders the form', async () => { - const { user } = setup() - const updateAddress = vi.fn() - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: updateAddress, - isLoading: false, - }) - render( - {}} - />, - { wrapper } - ) - await user.click(screen.getByTestId('edit-address')) - - expect( - screen.getByRole('button', { name: /update/i }) - ).toBeInTheDocument() - }) - - describe('when submitting', () => { - it('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.getByTestId('edit-address')) - await user.click(screen.queryByRole('button', { name: /update/i })!) - - expect(updateAddress).toHaveBeenCalled() - }) - }) - - describe('when the user clicks on cancel', () => { - it(`doesn't render the form anymore`, async () => { - const { user } = setup() - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: vi.fn(), - isLoading: false, - }) - render( - {}} - />, - { wrapper } - ) - - await user.click(screen.getByTestId('edit-address')) - await user.click(screen.getByRole('button', { name: /Cancel/ })) - - expect( - screen.queryByRole('button', { name: /save/i }) - ).not.toBeInTheDocument() - }) - }) - }) - - describe('when there is an error in the form', () => { - it('renders the error', async () => { - const { user } = setup() - const randomError = 'not a valid address' - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: vi.fn(), - error: randomError, - }) - render( - {}} - />, - { wrapper } - ) - - await user.click(screen.getByTestId('edit-address')) - - expect(screen.getByText(randomError)).toBeInTheDocument() - }) - }) - - describe('when the form is loading', () => { - it('has the error and save button disabled', async () => { - const { user } = setup() - mocks.useUpdateBillingAddress.mockReturnValue({ - mutate: vi.fn(), - isLoading: true, - }) - render( - {}} - />, - { wrapper } - ) - await user.click(screen.getByTestId('edit-address')) - - expect(screen.queryByRole('button', { name: /update/i })).toBeDisabled() - expect(screen.queryByRole('button', { name: /cancel/i })).toBeDisabled() - }) - }) -}) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.test.tsx index b3f7fdd2d6..bf8cb4a92e 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.test.tsx @@ -2,24 +2,36 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ThemeContextProvider } from 'shared/ThemeContext' -import { Plans } from 'shared/utils/billing' import PaymentMethod from './PaymentMethod' const mocks = vi.hoisted(() => ({ - useUpdateCard: vi.fn(), + useUpdatePaymentMethod: vi.fn(), })) vi.mock('services/account', async () => { const actual = await vi.importActual('services/account') return { ...actual, - useUpdateCard: mocks.useUpdateCard, + useUpdatePaymentMethod: mocks.useUpdatePaymentMethod, } }) const subscriptionDetail = { defaultPaymentMethod: { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + city: 'New York', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + postalCode: '10001', + state: 'NY', + }, + phone: '1234567890', + }, card: { brand: 'visa', expMonth: 12, @@ -27,20 +39,24 @@ const subscriptionDetail = { last4: '1234', }, }, - plan: { - value: Plans.USERS_PR_INAPPY, - }, currentPeriodEnd: 1606851492, cancelAtPeriodEnd: false, + customer: { + id: 'cust_123', + email: 'test@test.com', + }, + latestInvoice: null, + taxIds: [], + trialEnd: null, } -const wrapper = ({ children }) => ( +const wrapper: React.FC = ({ children }) => ( {children} ) // mocking all the stripe components; and trusting the library :) vi.mock('@stripe/react-stripe-js', () => { - function makeFakeComponent(name) { + function makeFakeComponent(name: string) { return function Component() { return name } @@ -54,7 +70,7 @@ vi.mock('@stripe/react-stripe-js', () => { } }) -describe('PaymentCard', () => { +describe('PaymentMethodCard', () => { function setup() { const user = userEvent.setup() @@ -70,85 +86,75 @@ describe('PaymentCard', () => { subscriptionDetail={null} provider="gh" owner="codecov" + setEditMode={() => {}} /> ) expect( screen.getByText( - /No credit card set. Please contact support if you think it’s an error or set it yourself./ + /No payment method set. Please contact support if you think it’s an error or set it yourself./ ) ).toBeInTheDocument() }) }) - describe(`when the user doesn't have any card`, () => { + describe(`when the user doesn't have any payment method`, () => { it('renders an error message', () => { + const subscriptionDetailMissingPaymentMethod = { + ...subscriptionDetail, + defaultPaymentMethod: { + ...subscriptionDetail.defaultPaymentMethod, + card: null, + usBankAccount: null, + }, + } render( {}} />, { wrapper } ) expect( screen.getByText( - /No credit card set. Please contact support if you think it’s an error or set it yourself./ + /No payment method set. Please contact support if you think it’s an error or set it yourself./ ) ).toBeInTheDocument() }) describe('when the user clicks on Set card', () => { - it(`doesn't render the card anymore`, async () => { + it(`doesn't render the card anymore and opens the form`, async () => { const { user } = setup() + const subscriptionDetailMissingPaymentMethod = { + ...subscriptionDetail, + defaultPaymentMethod: { + ...subscriptionDetail.defaultPaymentMethod, + card: null, + usBankAccount: null, + }, + } + const setEditMode = vi.fn() render( , { wrapper } ) - mocks.useUpdateCard.mockReturnValue({ + mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: () => null, isLoading: false, }) - await user.click(screen.getByTestId('open-modal')) + await user.click(screen.getByTestId('open-edit-mode')) expect(screen.queryByText(/Visa/)).not.toBeInTheDocument() - }) - - it('renders the form', async () => { - const { user } = setup() - render( - , - { wrapper } - ) - - mocks.useUpdateCard.mockReturnValue({ - mutate: () => null, - isLoading: false, - }) - await user.click(screen.getByTestId('open-modal')) - - expect( - screen.getByRole('button', { name: /update/i }) - ).toBeInTheDocument() + expect(setEditMode).toHaveBeenCalledWith(true) }) }) }) @@ -160,6 +166,7 @@ describe('PaymentCard', () => { subscriptionDetail={subscriptionDetail} provider="gh" owner="codecov" + setEditMode={() => {}} />, { wrapper } ) @@ -174,6 +181,7 @@ describe('PaymentCard', () => { subscriptionDetail={subscriptionDetail} provider="gh" owner="codecov" + setEditMode={() => {}} />, { wrapper } ) @@ -192,6 +200,7 @@ describe('PaymentCard', () => { }} provider="gh" owner="codecov" + setEditMode={() => {}} />, { wrapper } ) @@ -199,142 +208,4 @@ describe('PaymentCard', () => { expect(screen.queryByText(/1st December, 2020/)).not.toBeInTheDocument() }) }) - - describe('when the user clicks on Edit card', () => { - it(`doesn't render the card anymore`, async () => { - const { user } = setup() - const updateCard = vi.fn() - mocks.useUpdateCard.mockReturnValue({ - mutate: updateCard, - isLoading: false, - }) - - render( - , - { wrapper } - ) - await user.click(screen.getByTestId('edit-card')) - - expect(screen.queryByText(/Visa/)).not.toBeInTheDocument() - }) - - it('renders the form', async () => { - const { user } = setup() - const updateCard = vi.fn() - mocks.useUpdateCard.mockReturnValue({ - mutate: updateCard, - isLoading: false, - }) - render( - , - { wrapper } - ) - await user.click(screen.getByTestId('edit-card')) - - expect( - screen.getByRole('button', { name: /update/i }) - ).toBeInTheDocument() - }) - - describe('when submitting', () => { - it('calls the service to update the card', async () => { - const { user } = setup() - const updateCard = vi.fn() - mocks.useUpdateCard.mockReturnValue({ - mutate: updateCard, - isLoading: false, - }) - render( - , - { wrapper } - ) - await user.click(screen.getByTestId('edit-card')) - await user.click(screen.queryByRole('button', { name: /update/i })) - - expect(updateCard).toHaveBeenCalled() - }) - }) - - describe('when the user clicks on cancel', () => { - it(`doesn't render the form anymore`, async () => { - const { user } = setup() - mocks.useUpdateCard.mockReturnValue({ - mutate: vi.fn(), - isLoading: false, - }) - render( - , - { wrapper } - ) - - await user.click(screen.getByTestId('edit-card')) - await user.click(screen.getByRole('button', { name: /Cancel/ })) - - expect( - screen.queryByRole('button', { name: /save/i }) - ).not.toBeInTheDocument() - }) - }) - }) - - describe('when there is an error in the form', () => { - it('renders the error', async () => { - const { user } = setup() - const randomError = 'not rich enough' - mocks.useUpdateCard.mockReturnValue({ - mutate: vi.fn(), - error: { message: randomError }, - }) - render( - , - { wrapper } - ) - - await user.click(screen.getByTestId('edit-card')) - - expect(screen.getByText(randomError)).toBeInTheDocument() - }) - }) - - describe('when the form is loading', () => { - it('has the error and save button disabled', async () => { - const { user } = setup() - mocks.useUpdateCard.mockReturnValue({ - mutate: vi.fn(), - isLoading: true, - }) - render( - , - { wrapper } - ) - await user.click(screen.getByTestId('edit-card')) - - expect(screen.queryByRole('button', { name: /update/i })).toBeDisabled() - expect(screen.queryByRole('button', { name: /cancel/i })).toBeDisabled() - }) - }) }) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.tsx index c0fe83e00a..812c144055 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/ViewPaymentMethod/PaymentMethod/PaymentMethod.tsx @@ -39,6 +39,7 @@ function PaymentMethod({
{/* Address */} - = ({ children, }) => { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const prefersDark = + typeof window !== 'undefined' && + (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false) let systemTheme = Theme.LIGHT if (prefersDark) {