chore: tests for billings (#29720)

This commit is contained in:
Joel
2025-12-16 15:06:53 +08:00
committed by GitHub
parent e5cf0d0bf6
commit 47cd94ec3e
9 changed files with 603 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import Button from './button'
import { Plan } from '../../../type'
describe('CloudPlanButton', () => {
describe('Disabled state', () => {
test('should disable button and hide arrow when plan is not available', () => {
const handleGetPayUrl = jest.fn()
// Arrange
render(
<Button
plan={Plan.team}
isPlanDisabled
btnText="Get started"
handleGetPayUrl={handleGetPayUrl}
/>,
)
const button = screen.getByRole('button', { name: /Get started/i })
// Assert
expect(button).toBeDisabled()
expect(button.className).toContain('cursor-not-allowed')
expect(handleGetPayUrl).not.toHaveBeenCalled()
})
})
describe('Enabled state', () => {
test('should invoke handler and render arrow when plan is available', () => {
const handleGetPayUrl = jest.fn()
// Arrange
render(
<Button
plan={Plan.sandbox}
isPlanDisabled={false}
btnText="Start now"
handleGetPayUrl={handleGetPayUrl}
/>,
)
const button = screen.getByRole('button', { name: /Start now/i })
// Act
fireEvent.click(button)
// Assert
expect(handleGetPayUrl).toHaveBeenCalledTimes(1)
expect(button).not.toBeDisabled()
})
})
})

View File

@@ -0,0 +1,188 @@
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import CloudPlanItem from './index'
import { Plan } from '../../../type'
import { PlanRange } from '../../plan-switcher/plan-range-switcher'
import { useAppContext } from '@/context/app-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
import Toast from '../../../../base/toast'
import { ALL_PLANS } from '../../../config'
jest.mock('../../../../base/toast', () => ({
__esModule: true,
default: {
notify: jest.fn(),
},
}))
jest.mock('@/context/app-context', () => ({
useAppContext: jest.fn(),
}))
jest.mock('@/service/billing', () => ({
fetchBillingUrl: jest.fn(),
fetchSubscriptionUrls: jest.fn(),
}))
jest.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: jest.fn(),
}))
jest.mock('../../assets', () => ({
Sandbox: () => <div>Sandbox Icon</div>,
Professional: () => <div>Professional Icon</div>,
Team: () => <div>Team Icon</div>,
}))
const mockUseAppContext = useAppContext as jest.Mock
const mockUseAsyncWindowOpen = useAsyncWindowOpen as jest.Mock
const mockFetchBillingUrl = fetchBillingUrl as jest.Mock
const mockFetchSubscriptionUrls = fetchSubscriptionUrls as jest.Mock
const mockToastNotify = Toast.notify as jest.Mock
let assignedHref = ''
const originalLocation = window.location
beforeAll(() => {
Object.defineProperty(window, 'location', {
configurable: true,
value: {
get href() {
return assignedHref
},
set href(value: string) {
assignedHref = value
},
} as unknown as Location,
})
})
afterAll(() => {
Object.defineProperty(window, 'location', {
configurable: true,
value: originalLocation,
})
})
beforeEach(() => {
jest.clearAllMocks()
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true })
mockUseAsyncWindowOpen.mockReturnValue(jest.fn(async open => await open()))
mockFetchBillingUrl.mockResolvedValue({ url: 'https://billing.example' })
mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://subscription.example' })
assignedHref = ''
})
describe('CloudPlanItem', () => {
// Static content for each plan
describe('Rendering', () => {
test('should show plan metadata and free label for sandbox plan', () => {
render(
<CloudPlanItem
plan={Plan.sandbox}
currentPlan={Plan.sandbox}
planRange={PlanRange.monthly}
canPay
/>,
)
expect(screen.getByText('billing.plans.sandbox.name')).toBeInTheDocument()
expect(screen.getByText('billing.plans.sandbox.description')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.free')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' })).toBeInTheDocument()
})
test('should display yearly pricing with discount when planRange is yearly', () => {
render(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.sandbox}
planRange={PlanRange.yearly}
canPay
/>,
)
const professionalPlan = ALL_PLANS[Plan.professional]
expect(screen.getByText(`$${professionalPlan.price * 12}`)).toBeInTheDocument()
expect(screen.getByText(`$${professionalPlan.price * 10}`)).toBeInTheDocument()
expect(screen.getByText(/billing\.plansCommon\.priceTip.*billing\.plansCommon\.year/)).toBeInTheDocument()
})
test('should disable CTA when workspace already on higher tier', () => {
render(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.team}
planRange={PlanRange.monthly}
canPay
/>,
)
const button = screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })
expect(button).toBeDisabled()
})
})
// Payment actions triggered from the CTA
describe('Plan purchase flow', () => {
test('should show toast when non-manager tries to buy a plan', () => {
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: false })
render(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.sandbox}
planRange={PlanRange.monthly}
canPay
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' }))
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'billing.buyPermissionDeniedTip',
}))
expect(mockFetchBillingUrl).not.toHaveBeenCalled()
})
test('should open billing portal when upgrading current paid plan', async () => {
const openWindow = jest.fn(async (cb: () => Promise<string>) => await cb())
mockUseAsyncWindowOpen.mockReturnValue(openWindow)
render(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.professional}
planRange={PlanRange.monthly}
canPay
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' }))
await waitFor(() => {
expect(mockFetchBillingUrl).toHaveBeenCalledTimes(1)
})
expect(openWindow).toHaveBeenCalledTimes(1)
})
test('should redirect to subscription url when selecting a new paid plan', async () => {
render(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.sandbox}
planRange={PlanRange.monthly}
canPay
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' }))
await waitFor(() => {
expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'month')
expect(assignedHref).toBe('https://subscription.example')
})
})
})
})

View File

@@ -0,0 +1,30 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import List from './index'
import { Plan } from '../../../../type'
describe('CloudPlanItem/List', () => {
test('should show sandbox specific quotas', () => {
render(<List plan={Plan.sandbox} />)
expect(screen.getByText('billing.plansCommon.messageRequest.title:{"count":200}')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.triggerEvents.sandbox:{"count":3000}')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.startNodes.limited:{"count":2}')).toBeInTheDocument()
})
test('should show professional monthly quotas and tooltips', () => {
render(<List plan={Plan.professional} />)
expect(screen.getByText('billing.plansCommon.messageRequest.titlePerMonth:{"count":5000}')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.vectorSpaceTooltip')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.workflowExecution.faster')).toBeInTheDocument()
})
test('should show unlimited messaging details for team plan', () => {
render(<List plan={Plan.team} />)
expect(screen.getByText('billing.plansCommon.triggerEvents.unlimited')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.workflowExecution.priority')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.unlimitedApiRate')).toBeInTheDocument()
})
})