From 0df4e54b6606f6b5530817a1f967bb0b14201044 Mon Sep 17 00:00:00 2001 From: Suejung Shin Date: Thu, 30 Jan 2025 00:04:31 -0800 Subject: [PATCH] add tests --- .../{PlanPage.test.jsx => PlanPage.test.tsx} | 58 ++++++++- .../InfoMessageStripeCallback.test.tsx | 31 ++++- .../UpgradeForm/UpgradeForm.test.tsx | 117 +++++++++++++++--- .../UpgradeForm/UpgradeForm.tsx | 12 +- .../UpgradeForm/UpgradeFormModal.test.tsx | 79 ++++++++++++ 5 files changed, 267 insertions(+), 30 deletions(-) rename src/pages/PlanPage/{PlanPage.test.jsx => PlanPage.test.tsx} (79%) create mode 100644 src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeFormModal.test.tsx diff --git a/src/pages/PlanPage/PlanPage.test.jsx b/src/pages/PlanPage/PlanPage.test.tsx similarity index 79% rename from src/pages/PlanPage/PlanPage.test.jsx rename to src/pages/PlanPage/PlanPage.test.tsx index b7bbc12378..9b6e1afe51 100644 --- a/src/pages/PlanPage/PlanPage.test.jsx +++ b/src/pages/PlanPage/PlanPage.test.tsx @@ -13,6 +13,9 @@ import config from 'config' import { ThemeContextProvider } from 'shared/ThemeContext' +import { Location } from 'history' +import { UnverifiedPaymentMethodSchema } from 'services/account' +import { z } from 'zod' import PlanPage from './PlanPage' vi.mock('config') @@ -40,10 +43,10 @@ const queryClientV5 = new QueryClientV5({ defaultOptions: { queries: { retry: false } }, }) -let testLocation +let testLocation: Location const wrapper = (initialEntries = '') => - ({ children }) => ( + ({ children }: { children: React.ReactNode }) => ( @@ -79,7 +82,13 @@ afterAll(() => { describe('PlanPage', () => { function setup( - { owner, isSelfHosted = false } = { + { + owner, + isSelfHosted = false, + unverifiedPaymentMethods = [] as z.infer< + typeof UnverifiedPaymentMethodSchema + >[], + } = { owner: { username: 'codecov', isCurrentUserPartOfOrg: true, @@ -92,6 +101,17 @@ describe('PlanPage', () => { server.use( graphql.query('PlanPageData', () => { return HttpResponse.json({ data: { owner } }) + }), + graphql.query('UnverifiedPaymentMethods', () => { + return HttpResponse.json({ + data: { + owner: { + billing: { + unverifiedPaymentMethods, + }, + }, + }, + }) }) ) } @@ -102,7 +122,7 @@ describe('PlanPage', () => { owner: { username: 'codecov', isCurrentUserPartOfOrg: false, - numberOfUploads: null, + numberOfUploads: 0, }, }) }) @@ -120,7 +140,7 @@ describe('PlanPage', () => { owner: { username: 'codecov', isCurrentUserPartOfOrg: false, - numberOfUploads: null, + numberOfUploads: 0, }, }) }) @@ -149,6 +169,34 @@ describe('PlanPage', () => { const tabs = await screen.findByText(/Tabs/) expect(tabs).toBeInTheDocument() }) + + describe('when there are unverified payment methods', () => { + beforeEach(() => { + setup({ + owner: { + username: 'codecov', + isCurrentUserPartOfOrg: true, + numberOfUploads: 30, + }, + unverifiedPaymentMethods: [ + { + paymentMethodId: 'pm_123', + hostedVerificationUrl: 'https://verify.stripe.com', + }, + ], + }) + }) + + it('renders unverified payment method alert', async () => { + render(, { wrapper: wrapper('/plan/gh/codecov') }) + + const alert = await screen.findByText(/Verify Your New Payment Method/) + expect(alert).toBeInTheDocument() + + const link = screen.getByText('Click here') + expect(link).toHaveAttribute('href', 'https://verify.stripe.com') + }) + }) }) describe('testing routes', () => { diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/InfoMessageStripeCallback/InfoMessageStripeCallback.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/InfoMessageStripeCallback/InfoMessageStripeCallback.test.tsx index 68128812a7..e178ad8984 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/InfoMessageStripeCallback/InfoMessageStripeCallback.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/InfoMessageStripeCallback/InfoMessageStripeCallback.test.tsx @@ -12,9 +12,12 @@ const wrapper = describe('InfoMessageStripeCallback', () => { describe('when rendering without success or cancel in the url', () => { - const { container } = render(, { - wrapper: wrapper('/account/gh/codecov'), - }) + const { container } = render( + , + { + wrapper: wrapper('/account/gh/codecov'), + } + ) it('doesnt render anything', () => { expect(container).toBeEmptyDOMElement() @@ -23,13 +26,29 @@ describe('InfoMessageStripeCallback', () => { describe('when rendering with success in the url', () => { it('renders a success message', async () => { - render(, { - wrapper: wrapper('/account/gh/codecov?success'), - }) + render( + , + { + wrapper: wrapper('/account/gh/codecov?success'), + } + ) await expect( screen.getByText(/Subscription Update Successful/) ).toBeInTheDocument() }) }) + + describe('when hasUnverifiedPaymentMethods is true', () => { + it('does not enders a success message even at ?success', async () => { + const { container } = render( + , + { + wrapper: wrapper('/account/gh/codecov?success'), + } + ) + + expect(container).toBeEmptyDOMElement() + }) + }) }) diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx index 4a7ad77ffd..9e73679207 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx @@ -1,4 +1,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { + QueryClientProvider as QueryClientProviderV5, + QueryClient as QueryClientV5, +} from '@tanstack/react-queryV5' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { graphql, http, HttpResponse } from 'msw' @@ -233,6 +237,15 @@ const queryClient = new QueryClient({ }, }, }) + +const queryClientV5 = new QueryClientV5({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + const server = setupServer() beforeAll(() => { @@ -241,6 +254,7 @@ beforeAll(() => { afterEach(() => { queryClient.clear() + queryClientV5.clear() server.resetHandlers() vi.clearAllMocks() }) @@ -256,20 +270,22 @@ const wrapper: ( ) => React.FC = (initialEntries = ['/gh/codecov']) => ({ children }) => ( - - - - {children} - - { - testLocation = location - return null - }} - /> - - + + + + + {children} + + { + testLocation = location + return null + }} + /> + + + ) type SetupArgs = { @@ -281,6 +297,7 @@ type SetupArgs = { hasSentryPlans?: boolean monthlyPlan?: boolean planUserCount?: number + hasUnverifiedPaymentMethod?: boolean } describe('UpgradeForm', () => { @@ -293,6 +310,7 @@ describe('UpgradeForm', () => { hasSentryPlans = false, monthlyPlan = true, planUserCount = 1, + hasUnverifiedPaymentMethod = false, }: SetupArgs) { const addNotification = vi.fn() const user = userEvent.setup() @@ -384,6 +402,24 @@ describe('UpgradeForm', () => { }, }, }) + }), + graphql.query('UnverifiedPaymentMethods', () => { + return HttpResponse.json({ + data: { + owner: { + billing: { + unverifiedPaymentMethods: hasUnverifiedPaymentMethod + ? [ + { + paymentMethodId: 'asdf', + hostedVerficationUrl: 'https://stripe.com', + }, + ] + : null, + }, + }, + }, + }) }) ) @@ -391,6 +427,59 @@ describe('UpgradeForm', () => { } describe('when rendered', () => { + describe('when user has unverified payment methods', () => { + const props = { + setSelectedPlan: vi.fn(), + selectedPlan: proPlanYear, + } + + it('shows modal when form is submitted', async () => { + const { user } = setup({ + planValue: Plans.USERS_BASIC, + hasUnverifiedPaymentMethod: true, + }) + render(, { wrapper: wrapper() }) + + const proceedToCheckoutButton = await screen.findByRole('button', { + name: /Proceed to checkout/, + }) + await user.click(proceedToCheckoutButton) + + const modal = await screen.findByText( + /Are you sure you want to abandon this upgrade and start a new one/, + { + exact: false, + } + ) + expect(modal).toBeInTheDocument() + }) + + it('does not show modal when no unverified payment methods', async () => { + const { user } = setup({ + planValue: Plans.USERS_BASIC, + hasUnverifiedPaymentMethod: false, + }) + render(, { wrapper: wrapper() }) + + const input = await screen.findByRole('spinbutton') + await user.type(input, '{backspace}{backspace}{backspace}') + await user.type(input, '20') + + const proceedToCheckoutButton = await screen.findByRole('button', { + name: /Proceed to checkout/, + }) + await user.click(proceedToCheckoutButton) + + const modal = screen.queryByText( + /Are you sure you want to abandon this upgrade and start a new one/, + { + exact: false, + } + ) + expect(modal).not.toBeInTheDocument() + }) + }) + describe('when the user has a basic plan', () => { const props = { setSelectedPlan: vi.fn(), diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx index 6eb035c3fc..519ea65438 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx @@ -8,6 +8,7 @@ import { useAccountDetails, useAvailablePlans, usePlanData, + useUnverifiedPaymentMethods, } from 'services/account' import { Provider } from 'shared/api/helpers' import { canApplySentryUpgrade, getNextBillingDate } from 'shared/utils/billing' @@ -45,6 +46,10 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) { const { data: accountDetails } = useAccountDetails({ provider, owner }) const { data: plans } = useAvailablePlans({ provider, owner }) const { data: planData } = usePlanData({ owner, provider }) + const { data: unverifiedPaymentMethods } = useUnverifiedPaymentMethods({ + provider, + owner, + }) const { upgradePlan } = useUpgradeControls() const [showModal, setShowModal] = useState(false) const [formData, setFormData] = useState() @@ -95,7 +100,7 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) { }, [newPlan, trigger]) const onSubmit = handleSubmit((data) => { - if (accountDetails?.unverifiedPaymentMethods?.length) { + if (unverifiedPaymentMethods?.length) { setFormData(data) setShowModal(true) } else { @@ -141,10 +146,7 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) { setIsUpgrading(true) upgradePlan(formData) }} - url={ - accountDetails?.unverifiedPaymentMethods?.[0] - ?.hostedVerificationLink || '' - } + url={unverifiedPaymentMethods?.[0]?.hostedVerificationUrl || ''} isUpgrading={isUpgrading} /> )} diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeFormModal.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeFormModal.test.tsx new file mode 100644 index 0000000000..28aadfd0f4 --- /dev/null +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeFormModal.test.tsx @@ -0,0 +1,79 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import UpgradeFormModal from './UpgradeFormModal' + +describe('UpgradeFormModal', () => { + const mockOnClose = vi.fn() + const mockOnConfirm = vi.fn() + const mockUrl = 'https://verify.stripe.com' + + const setup = (isUpgrading = false) => { + return render( + + ) + } + + beforeEach(() => { + vi.resetAllMocks() + }) + + it('renders modal with correct content', () => { + setup() + + expect(screen.getByText('Incomplete Plan Upgrade')).toBeInTheDocument() + expect( + screen.getByText( + /You have a pending plan upgrade awaiting payment verification/ + ) + ).toBeInTheDocument() + expect(screen.getByText('here')).toHaveAttribute('href', mockUrl) + expect( + screen.getByText( + /Are you sure you want to abandon this upgrade and start a new one/ + ) + ).toBeInTheDocument() + }) + + it('calls onClose when cancel button is clicked', async () => { + setup() + const utils = userEvent.setup() + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }) + await utils.click(cancelButton) + + expect(mockOnClose).toHaveBeenCalled() + }) + + it('calls onConfirm when confirm button is clicked', async () => { + setup() + const utils = userEvent.setup() + + const confirmButton = screen.getByRole('button', { + name: 'Yes, Start New Upgrade', + }) + await utils.click(confirmButton) + + expect(mockOnConfirm).toHaveBeenCalled() + }) + + describe('when isUpgrading is true', () => { + it('disables buttons and shows processing text', () => { + setup(true) + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }) + const confirmButton = screen.getByRole('button', { + name: 'Processing...', + }) + + expect(cancelButton).toBeDisabled() + expect(confirmButton).toBeDisabled() + }) + }) +})