Skip to content

Commit

Permalink
feat: Add activation banner for trial eligible owners 1/4 (#2817)
Browse files Browse the repository at this point in the history
* feat: Add activation banner for trial eligible owners

* pull out interface + spec stuff

* Refactor CircleCI repo onboarding into one file (#2806)

* Refactor Other CI repo onboarding into one file (#2807)

* Update repo onboarding title position and page alignment (#2818)

* sec: 390 - Add validation for potential XSS vuln (#2797)

* add tests, and validation for provider

* add back supportServiceless param

* ref: 1548 Part 1: Convert all Header files to TS (#2821)

* ref all header files to TS

* remove prop types and rebase

* fix: Remove repository from GUT settings page header (#2823)

Small tweak removing `repository` from the GUT settings page.

* Install radix-ui react radio group (#2825)

* Update repo onboarding steps with new Card component (#2819)

GH codecov/engineering-team#1665

* Update tests

* Update to correct import orders

* Update tests

---------

Co-authored-by: Spencer Murray <[email protected]>
Co-authored-by: ajay-sentry <[email protected]>
Co-authored-by: nicholas-codecov <[email protected]>
  • Loading branch information
4 people authored May 2, 2024
1 parent b5abb62 commit bfae2b1
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import { graphql } from 'msw'
import { setupServer } from 'msw/node'
import { MemoryRouter, Route } from 'react-router-dom'

import ActivationBanner from './ActivationBanner'

jest.mock('./TrialEligibleBanner', () => () => 'TrialEligibleBanner')

const queryClient = new QueryClient()

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/gh/codecov/gazebo/new']}>
<Route path="/:provider/:owner/:repo/new">{children}</Route>
</MemoryRouter>
</QueryClientProvider>
)

const server = setupServer()

beforeAll(() => {
server.listen()
})
afterEach(() => {
queryClient.clear()
server.resetHandlers()
})
afterAll(() => {
server.close()
})

const mockTrialData = {
baseUnitPrice: 10,
benefits: [],
billingRate: 'monthly',
marketingName: 'Users Basic',
monthlyUploadLimit: 250,
value: 'users-basic',
trialStatus: 'ONGOING',
trialStartDate: '2023-01-01T08:55:25',
trialEndDate: '2023-01-10T08:55:25',
trialTotalDays: 0,
pretrialUsersCount: 0,
planUserCount: 1,
}

describe('ActivationBanner', () => {
function setup(
privateRepos = true,
trialStatus = 'NOT_STARTED',
value = 'users-basic'
) {
server.use(
graphql.query('GetPlanData', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.data({
owner: {
hasPrivateRepos: privateRepos,
plan: {
...mockTrialData,
trialStatus,
value,
},
pretrialPlan: {
baseUnitPrice: 10,
benefits: [],
billingRate: 'monthly',
marketingName: 'Users Basic',
monthlyUploadLimit: 250,
value: 'users-basic',
},
},
})
)
})
)
}

it('renders trial eligible banner if user is eligible to trial', async () => {
setup()
render(<ActivationBanner />, { wrapper })

const trialEligibleBanner = await screen.findByText(/TrialEligibleBanner/)
expect(trialEligibleBanner).toBeInTheDocument()
})

it('does not render trial eligible banner if user is not eligible to trial', async () => {
setup(false)
const { container } = render(<ActivationBanner />, { wrapper })

await waitFor(() => queryClient.isFetching)
await waitFor(() => !queryClient.isFetching)

expect(container).toBeEmptyDOMElement()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useParams } from 'react-router-dom'

import { TrialStatuses, usePlanData } from 'services/account'
import { isBasicPlan } from 'shared/utils/billing'

import TrialEligibleBanner from './TrialEligibleBanner'

interface URLParams {
provider: string
owner: string
}

function ActivationBanner() {
const { owner, provider } = useParams<URLParams>()
const { data: planData } = usePlanData({
owner,
provider,
})
const isNewTrial = planData?.plan?.trialStatus === TrialStatuses.NOT_STARTED
const isTrialEligible =
isBasicPlan(planData?.plan?.value) &&
planData?.hasPrivateRepos &&
isNewTrial

if (!isTrialEligible) {
return null
}

return (
<div className="mt-4">
<TrialEligibleBanner />
</div>
)
}

export default ActivationBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { graphql } from 'msw'
import { setupServer } from 'msw/node'
import { MemoryRouter, Route } from 'react-router-dom'

import TrialEligibleBanner from './TrialEligibleBanner'

const queryClient = new QueryClient()

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/gh/codecov/gazebo/new']}>
<Route path="/:provider/:owner/:repo/new">{children}</Route>
</MemoryRouter>
</QueryClientProvider>
)

const server = setupServer()

beforeAll(() => {
server.listen()
})
afterEach(() => {
queryClient.clear()
server.resetHandlers()
})
afterAll(() => {
server.close()
})

describe('TrialEligibleBanner', () => {
function setup() {
const mockTrialMutationVariables = jest.fn()
const user = userEvent.setup()
server.use(
graphql.mutation('startTrial', (req, res, ctx) => {
mockTrialMutationVariables(req?.variables)

return res(ctx.status(200))
})
)

return { mockTrialMutationVariables, user }
}

it('renders the banner with correct content', () => {
setup()
render(<TrialEligibleBanner />, { wrapper })

const bannerHeading = screen.getByRole('heading', {
name: /start a free 14-day trial on pro team plan/i,
})
expect(bannerHeading).toBeInTheDocument()

const list = screen.getByText(/Unlimited members/i)
expect(list).toBeInTheDocument()
})

it('renders correct links', () => {
setup()
render(<TrialEligibleBanner />, { wrapper })

const upgradeLink = screen.getByRole('link', { name: /upgrade/ })
expect(upgradeLink).toBeInTheDocument()
expect(upgradeLink).toHaveAttribute('href', '/plan/gh/codecov/upgrade')

const manageMembersLink = screen.getByRole('link', {
name: /manage members/,
})
expect(manageMembersLink).toBeInTheDocument()
expect(manageMembersLink).toHaveAttribute('href', '/members/gh/codecov')
})

it('calls the start trial function when the "Start Trial" button is clicked', async () => {
const { mockTrialMutationVariables, user } = setup()
render(<TrialEligibleBanner />, { wrapper })

const startTrialButton = screen.getByRole('button', {
name: /Start Trial/,
})
await user.click(startTrialButton)

await waitFor(() =>
expect(mockTrialMutationVariables).toHaveBeenCalledWith({
input: { orgUsername: 'codecov' },
})
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useParams } from 'react-router-dom'

import { useStartTrial } from 'services/trial'
import A from 'ui/A'
import Banner from 'ui/Banner'
import BannerContent from 'ui/Banner/BannerContent'
import BannerHeading from 'ui/Banner/BannerHeading'
import Button from 'ui/Button'

interface URLParams {
owner: string
}

function TrialEligibleBanner() {
const { owner } = useParams<URLParams>()
const { mutate: fireTrial, isLoading } = useStartTrial()

return (
<Banner variant="plain">
<BannerContent>
<BannerHeading>
<h2 className="font-semibold">
Start a free 14-day trial on Pro Team plan &#128640;
</h2>
</BannerHeading>
<div className="flex justify-between">
<ul className="mb-2 list-inside list-disc">
<li>Unlimited members</li>
<li>Unlimited repos</li>
<li>Unlimited uploads</li>
<li>Access to all features</li>
<li>No credit card required</li>
</ul>
<div className="flex items-start justify-end">
<Button
onClick={() => fireTrial({ owner })}
hook="trial-eligible-banner-start-trial"
to={undefined}
disabled={isLoading}
variant="primary"
>
Start Trial
</Button>
</div>
</div>
<span className="text-ds-gray-quinary">
Plan limits reached, you can{' '}
<A
to={{
pageName: 'upgradeOrgPlan',
}}
hook="trial-eligible-banner-to-upgrade-page"
isExternal={false}
>
upgrade
</A>{' '}
or{' '}
<A
to={{
pageName: 'membersTab',
}}
hook="trial-eligible-banner-to-manage-members-page"
isExternal={false}
>
manage members
</A>
.
</span>
</BannerContent>
</Banner>
)
}

export default TrialEligibleBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './TrialEligibleBanner'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ActivationBanner'
9 changes: 9 additions & 0 deletions src/pages/RepoPage/CoverageOnboarding/NewRepoTab.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jest.mock('shared/useRedirect')
const mockedUseRedirect = useRedirect as jest.Mock
jest.mock('./GitHubActions', () => () => 'GitHubActions')
jest.mock('./OtherCI', () => () => 'OtherCI')
jest.mock('./ActivationBanner', () => () => 'ActivationBanner')


const mockCurrentUser = {
me: {
Expand Down Expand Up @@ -137,6 +139,13 @@ describe('NewRepoTab', () => {
expect(header).toBeInTheDocument()
})

it('renders ActivationBanner', async () => {
render(<NewRepoTab />, { wrapper: wrapper() })

const banner = await screen.findByText('ActivationBanner')
expect(banner).toBeInTheDocument()
})

describe('users provider is github', () => {
it('renders github actions tab', async () => {
render(<NewRepoTab />, { wrapper: wrapper() })
Expand Down
2 changes: 2 additions & 0 deletions src/pages/RepoPage/CoverageOnboarding/NewRepoTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { providerToName } from 'shared/utils'
import Spinner from 'ui/Spinner'
import TabNavigation from 'ui/TabNavigation'

import ActivationBanner from './ActivationBanner'
import CircleCI from './CircleCI'
import GitHubActions from './GitHubActions'
import IntroBlurb from './IntroBlurb'
Expand Down Expand Up @@ -43,6 +44,7 @@ function Content({ provider }: { provider: string }) {
{ pageName: 'newOtherCI' },
]}
/>
<ActivationBanner />
<div className="mt-6">
<Switch>
<SentryRoute path="/:provider/:owner/:repo/new" exact>
Expand Down

0 comments on commit bfae2b1

Please sign in to comment.