refactor(web): migrate to Vitest and esm (#29974)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-22 16:35:22 +08:00
committed by GitHub
parent 42f7ecda12
commit eabdc5f0eb
268 changed files with 5455 additions and 6307 deletions

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'
import AnnotationFull from './index'
jest.mock('./usage', () => ({
vi.mock('./usage', () => ({
__esModule: true,
default: (props: { className?: string }) => {
return (
@@ -12,7 +12,7 @@ jest.mock('./usage', () => ({
},
}))
jest.mock('../upgrade-btn', () => ({
vi.mock('../upgrade-btn', () => ({
__esModule: true,
default: (props: { loc?: string }) => {
return (
@@ -25,7 +25,7 @@ jest.mock('../upgrade-btn', () => ({
describe('AnnotationFull', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// Rendering marketing copy with action button

View File

@@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import AnnotationFullModal from './modal'
jest.mock('./usage', () => ({
vi.mock('./usage', () => ({
__esModule: true,
default: (props: { className?: string }) => {
return (
@@ -13,7 +13,7 @@ jest.mock('./usage', () => ({
}))
let mockUpgradeBtnProps: { loc?: string } | null = null
jest.mock('../upgrade-btn', () => ({
vi.mock('../upgrade-btn', () => ({
__esModule: true,
default: (props: { loc?: string }) => {
mockUpgradeBtnProps = props
@@ -31,7 +31,7 @@ type ModalSnapshot = {
className?: string
}
let mockModalProps: ModalSnapshot | null = null
jest.mock('../../base/modal', () => ({
vi.mock('../../base/modal', () => ({
__esModule: true,
default: ({ isShow, children, onClose, closable, className }: { isShow: boolean; children: React.ReactNode; onClose: () => void; closable?: boolean; className?: string }) => {
mockModalProps = {
@@ -56,7 +56,7 @@ jest.mock('../../base/modal', () => ({
describe('AnnotationFullModal', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockUpgradeBtnProps = null
mockModalProps = null
})
@@ -65,7 +65,7 @@ describe('AnnotationFullModal', () => {
describe('Rendering', () => {
it('should display main info when visible', () => {
// Act
render(<AnnotationFullModal show onHide={jest.fn()} />)
render(<AnnotationFullModal show onHide={vi.fn()} />)
// Assert
expect(screen.getByText('billing.annotatedResponse.fullTipLine1')).toBeInTheDocument()
@@ -85,7 +85,7 @@ describe('AnnotationFullModal', () => {
describe('Visibility', () => {
it('should not render content when hidden', () => {
// Act
const { container } = render(<AnnotationFullModal show={false} onHide={jest.fn()} />)
const { container } = render(<AnnotationFullModal show={false} onHide={vi.fn()} />)
// Assert
expect(container).toBeEmptyDOMElement()
@@ -97,7 +97,7 @@ describe('AnnotationFullModal', () => {
describe('Close handling', () => {
it('should trigger onHide when close control is clicked', () => {
// Arrange
const onHide = jest.fn()
const onHide = vi.fn()
// Act
render(<AnnotationFullModal show onHide={onHide} />)

View File

@@ -3,9 +3,9 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import PlanUpgradeModal from './index'
const mockSetShowPricingModal = jest.fn()
const mockSetShowPricingModal = vi.fn()
jest.mock('@/app/components/base/modal', () => {
vi.mock('@/app/components/base/modal', () => {
const MockModal = ({ isShow, children }: { isShow: boolean; children: React.ReactNode }) => (
isShow ? <div data-testid="plan-upgrade-modal">{children}</div> : null
)
@@ -15,7 +15,7 @@ jest.mock('@/app/components/base/modal', () => {
}
})
jest.mock('@/context/modal-context', () => ({
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
}),
@@ -25,7 +25,7 @@ const baseProps = {
title: 'Upgrade Required',
description: 'You need to upgrade your plan.',
show: true,
onClose: jest.fn(),
onClose: vi.fn(),
}
const renderComponent = (props: Partial<React.ComponentProps<typeof PlanUpgradeModal>> = {}) => {
@@ -35,7 +35,7 @@ const renderComponent = (props: Partial<React.ComponentProps<typeof PlanUpgradeM
describe('PlanUpgradeModal', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// Rendering and props-driven content
@@ -68,7 +68,7 @@ describe('PlanUpgradeModal', () => {
it('should call onClose when dismiss button is clicked', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
const onClose = vi.fn()
renderComponent({ onClose })
// Act
@@ -82,8 +82,8 @@ describe('PlanUpgradeModal', () => {
it('should call onUpgrade and onClose when upgrade button is clicked with onUpgrade provided', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
const onUpgrade = jest.fn()
const onClose = vi.fn()
const onUpgrade = vi.fn()
renderComponent({ onClose, onUpgrade })
// Act
@@ -99,7 +99,7 @@ describe('PlanUpgradeModal', () => {
it('should open pricing modal when upgrade button is clicked without onUpgrade', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
const onClose = vi.fn()
renderComponent({ onClose, onUpgrade: undefined })
// Act

View File

@@ -154,7 +154,10 @@ describe('Enterprise Icon Component', () => {
describe('CSS Variables', () => {
it('should use CSS custom properties for colors', () => {
const { container } = render(<Enterprise />)
const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
const allFillElements = container.querySelectorAll('[fill]')
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
el.getAttribute('fill')?.startsWith('var('),
)
expect(elementsWithCSSVars.length).toBeGreaterThan(0)
})

View File

@@ -119,7 +119,10 @@ describe('Professional Icon Component', () => {
describe('CSS Variables', () => {
it('should use CSS custom properties for colors', () => {
const { container } = render(<Professional />)
const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
const allFillElements = container.querySelectorAll('[fill]')
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
el.getAttribute('fill')?.startsWith('var('),
)
// All fill attributes should use CSS variables
expect(elementsWithCSSVars.length).toBeGreaterThan(0)

View File

@@ -110,7 +110,10 @@ describe('Sandbox Icon Component', () => {
describe('CSS Variables', () => {
it('should use CSS custom properties for colors', () => {
const { container } = render(<Sandbox />)
const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
const allFillElements = container.querySelectorAll('[fill]')
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
el.getAttribute('fill')?.startsWith('var('),
)
// All fill attributes should use CSS variables
expect(elementsWithCSSVars.length).toBeGreaterThan(0)

View File

@@ -133,7 +133,10 @@ describe('Team Icon Component', () => {
describe('CSS Variables', () => {
it('should use CSS custom properties for colors', () => {
const { container } = render(<Team />)
const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
const allFillElements = container.querySelectorAll('[fill]')
const elementsWithCSSVars = Array.from(allFillElements).filter(el =>
el.getAttribute('fill')?.startsWith('var('),
)
expect(elementsWithCSSVars.length).toBeGreaterThan(0)
})

View File

@@ -6,7 +6,7 @@ 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()
const handleGetPayUrl = vi.fn()
// Arrange
render(
<Button
@@ -27,7 +27,7 @@ describe('CloudPlanButton', () => {
describe('Enabled state', () => {
test('should invoke handler and render arrow when plan is available', () => {
const handleGetPayUrl = jest.fn()
const handleGetPayUrl = vi.fn()
// Arrange
render(
<Button

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import CloudPlanItem from './index'
@@ -9,37 +10,37 @@ import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
import Toast from '../../../../base/toast'
import { ALL_PLANS } from '../../../config'
jest.mock('../../../../base/toast', () => ({
vi.mock('../../../../base/toast', () => ({
__esModule: true,
default: {
notify: jest.fn(),
notify: vi.fn(),
},
}))
jest.mock('@/context/app-context', () => ({
useAppContext: jest.fn(),
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
jest.mock('@/service/billing', () => ({
fetchBillingUrl: jest.fn(),
fetchSubscriptionUrls: jest.fn(),
vi.mock('@/service/billing', () => ({
fetchBillingUrl: vi.fn(),
fetchSubscriptionUrls: vi.fn(),
}))
jest.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: jest.fn(),
vi.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: vi.fn(),
}))
jest.mock('../../assets', () => ({
vi.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
const mockUseAppContext = useAppContext as Mock
const mockUseAsyncWindowOpen = useAsyncWindowOpen as Mock
const mockFetchBillingUrl = fetchBillingUrl as Mock
const mockFetchSubscriptionUrls = fetchSubscriptionUrls as Mock
const mockToastNotify = Toast.notify as Mock
let assignedHref = ''
const originalLocation = window.location
@@ -66,9 +67,9 @@ afterAll(() => {
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true })
mockUseAsyncWindowOpen.mockReturnValue(jest.fn(async open => await open()))
mockUseAsyncWindowOpen.mockReturnValue(vi.fn(async open => await open()))
mockFetchBillingUrl.mockResolvedValue({ url: 'https://billing.example' })
mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://subscription.example' })
assignedHref = ''
@@ -147,7 +148,7 @@ describe('CloudPlanItem', () => {
})
test('should open billing portal when upgrading current paid plan', async () => {
const openWindow = jest.fn(async (cb: () => Promise<string>) => await cb())
const openWindow = vi.fn(async (cb: () => Promise<string>) => await cb())
mockUseAsyncWindowOpen.mockReturnValue(openWindow)
render(

View File

@@ -3,7 +3,7 @@ import Item from './index'
describe('Item', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// Rendering the plan item row

View File

@@ -3,7 +3,7 @@ import Tooltip from './tooltip'
describe('Tooltip', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// Rendering the info tooltip container

View File

@@ -3,19 +3,22 @@ import { render, screen } from '@testing-library/react'
import Plans from './index'
import { Plan, type UsagePlanInfo } from '../../type'
import { PlanRange } from '../plan-switcher/plan-range-switcher'
import cloudPlanItem from './cloud-plan-item'
import selfHostedPlanItem from './self-hosted-plan-item'
import type { Mock } from 'vitest'
jest.mock('./cloud-plan-item', () => ({
vi.mock('./cloud-plan-item', () => ({
__esModule: true,
default: jest.fn(props => (
default: vi.fn(props => (
<div data-testid={`cloud-plan-${props.plan}`} data-current-plan={props.currentPlan}>
Cloud {props.plan}
</div>
)),
}))
jest.mock('./self-hosted-plan-item', () => ({
vi.mock('./self-hosted-plan-item', () => ({
__esModule: true,
default: jest.fn(props => (
default: vi.fn(props => (
<div data-testid={`self-plan-${props.plan}`}>
Self {props.plan}
</div>
@@ -56,8 +59,7 @@ describe('Plans', () => {
expect(screen.getByTestId('cloud-plan-professional')).toBeInTheDocument()
expect(screen.getByTestId('cloud-plan-team')).toBeInTheDocument()
const cloudPlanItem = jest.requireMock('./cloud-plan-item').default as jest.Mock
const firstCallProps = cloudPlanItem.mock.calls[0][0]
const firstCallProps = (cloudPlanItem as unknown as Mock).mock.calls[0][0]
expect(firstCallProps.plan).toBe(Plan.sandbox)
// Enterprise should be normalized to team when passed down
expect(firstCallProps.currentPlan).toBe(Plan.team)
@@ -80,8 +82,7 @@ describe('Plans', () => {
expect(screen.getByTestId('self-plan-premium')).toBeInTheDocument()
expect(screen.getByTestId('self-plan-enterprise')).toBeInTheDocument()
const selfPlanItem = jest.requireMock('./self-hosted-plan-item').default as jest.Mock
expect(selfPlanItem).toHaveBeenCalledTimes(3)
expect(selfHostedPlanItem).toHaveBeenCalledTimes(3)
})
})
})

View File

@@ -1,3 +1,4 @@
import type { MockedFunction } from 'vitest'
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import Button from './button'
@@ -5,23 +6,18 @@ import { SelfHostedPlan } from '../../../type'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
jest.mock('@/hooks/use-theme')
vi.mock('@/hooks/use-theme')
jest.mock('@/app/components/base/icons/src/public/billing', () => ({
AwsMarketplaceLight: () => <div>AwsMarketplaceLight</div>,
AwsMarketplaceDark: () => <div>AwsMarketplaceDark</div>,
}))
const mockUseTheme = useTheme as jest.MockedFunction<typeof useTheme>
const mockUseTheme = useTheme as MockedFunction<typeof useTheme>
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockUseTheme.mockReturnValue({ theme: Theme.light } as unknown as ReturnType<typeof useTheme>)
})
describe('SelfHostedPlanButton', () => {
test('should invoke handler when clicked', () => {
const handleGetPayUrl = jest.fn()
const handleGetPayUrl = vi.fn()
render(
<Button
plan={SelfHostedPlan.community}
@@ -33,29 +29,19 @@ describe('SelfHostedPlanButton', () => {
expect(handleGetPayUrl).toHaveBeenCalledTimes(1)
})
test('should render AWS marketplace badge for premium plan in light theme', () => {
const handleGetPayUrl = jest.fn()
test.each([
{ label: 'light', theme: Theme.light },
{ label: 'dark', theme: Theme.dark },
])('should render premium button label when theme is $label', ({ theme }) => {
mockUseTheme.mockReturnValue({ theme } as unknown as ReturnType<typeof useTheme>)
render(
<Button
plan={SelfHostedPlan.premium}
handleGetPayUrl={handleGetPayUrl}
handleGetPayUrl={vi.fn()}
/>,
)
expect(screen.getByText('AwsMarketplaceLight')).toBeInTheDocument()
})
test('should switch to dark AWS badge in dark theme', () => {
mockUseTheme.mockReturnValue({ theme: Theme.dark } as unknown as ReturnType<typeof useTheme>)
render(
<Button
plan={SelfHostedPlan.premium}
handleGetPayUrl={jest.fn()}
/>,
)
expect(screen.getByText('AwsMarketplaceDark')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'billing.plans.premium.btnText' })).toBeInTheDocument()
})
})

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import SelfHostedPlanItem from './index'
@@ -12,7 +13,7 @@ const featuresTranslations: Record<string, string[]> = {
'billing.plans.enterprise.features': ['enterprise-feature-1'],
}
jest.mock('react-i18next', () => ({
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (options?.returnObjects)
@@ -23,18 +24,18 @@ jest.mock('react-i18next', () => ({
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,
}))
jest.mock('../../../../base/toast', () => ({
vi.mock('../../../../base/toast', () => ({
__esModule: true,
default: {
notify: jest.fn(),
notify: vi.fn(),
},
}))
jest.mock('@/context/app-context', () => ({
useAppContext: jest.fn(),
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
jest.mock('../../assets', () => ({
vi.mock('../../assets', () => ({
Community: () => <div>Community Icon</div>,
Premium: () => <div>Premium Icon</div>,
Enterprise: () => <div>Enterprise Icon</div>,
@@ -42,15 +43,8 @@ jest.mock('../../assets', () => ({
EnterpriseNoise: () => <div>EnterpriseNoise</div>,
}))
jest.mock('@/app/components/base/icons/src/public/billing', () => ({
Azure: () => <div>Azure</div>,
GoogleCloud: () => <div>Google Cloud</div>,
AwsMarketplaceDark: () => <div>AwsMarketplaceDark</div>,
AwsMarketplaceLight: () => <div>AwsMarketplaceLight</div>,
}))
const mockUseAppContext = useAppContext as jest.Mock
const mockToastNotify = Toast.notify as jest.Mock
const mockUseAppContext = useAppContext as Mock
const mockToastNotify = Toast.notify as Mock
let assignedHref = ''
const originalLocation = window.location
@@ -77,7 +71,7 @@ afterAll(() => {
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true })
assignedHref = ''
})
@@ -100,8 +94,6 @@ describe('SelfHostedPlanItem', () => {
expect(screen.getByText('billing.plans.premium.price')).toBeInTheDocument()
expect(screen.getByText('billing.plans.premium.comingSoon')).toBeInTheDocument()
expect(screen.getByText('Azure')).toBeInTheDocument()
expect(screen.getByText('Google Cloud')).toBeInTheDocument()
})
})

View File

@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'
import List from './index'
import { SelfHostedPlan } from '@/app/components/billing/type'
jest.mock('react-i18next', () => ({
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (options?.returnObjects)

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import UpgradeBtn from './index'
@@ -6,20 +7,20 @@ import UpgradeBtn from './index'
// PremiumBadge, Button, SparklesSoft are all base components
// ✅ Mock external dependencies only
const mockSetShowPricingModal = jest.fn()
jest.mock('@/context/modal-context', () => ({
const mockSetShowPricingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
}),
}))
// Mock gtag for tracking tests
let mockGtag: jest.Mock | undefined
let mockGtag: Mock | undefined
describe('UpgradeBtn', () => {
beforeEach(() => {
jest.clearAllMocks()
mockGtag = jest.fn()
vi.clearAllMocks()
mockGtag = vi.fn()
;(window as any).gtag = mockGtag
})
@@ -110,7 +111,7 @@ describe('UpgradeBtn', () => {
it('should apply custom style to premium badge', () => {
// Arrange
const customStyle = { backgroundColor: 'red', padding: '10px' }
const customStyle = { padding: '10px' }
// Act
const { container } = render(<UpgradeBtn style={customStyle} />)
@@ -122,7 +123,7 @@ describe('UpgradeBtn', () => {
it('should apply custom style to plain button', () => {
// Arrange
const customStyle = { backgroundColor: 'blue', margin: '5px' }
const customStyle = { margin: '5px' }
// Act
render(<UpgradeBtn isPlain style={customStyle} />)
@@ -162,7 +163,7 @@ describe('UpgradeBtn', () => {
it('should call custom onClick when provided and premium badge is clicked', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn onClick={handleClick} />)
@@ -177,7 +178,7 @@ describe('UpgradeBtn', () => {
it('should call custom onClick when provided and plain button is clicked', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn isPlain onClick={handleClick} />)
@@ -279,7 +280,7 @@ describe('UpgradeBtn', () => {
it('should call both custom onClick and track gtag when both are provided', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
const loc = 'settings-page'
// Act
@@ -409,7 +410,7 @@ describe('UpgradeBtn', () => {
it('should handle all custom props together', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
const customStyle = { margin: '10px' }
const customClass = 'all-custom'
@@ -445,7 +446,7 @@ describe('UpgradeBtn', () => {
it('should be keyboard accessible with plain button', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn isPlain onClick={handleClick} />)
@@ -465,7 +466,7 @@ describe('UpgradeBtn', () => {
it('should be keyboard accessible with Space key', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn isPlain onClick={handleClick} />)
@@ -481,7 +482,7 @@ describe('UpgradeBtn', () => {
it('should be clickable for premium badge variant', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn onClick={handleClick} />)
@@ -524,7 +525,7 @@ describe('UpgradeBtn', () => {
it('should integrate onClick with analytics tracking', async () => {
// Arrange
const user = userEvent.setup()
const handleClick = jest.fn()
const handleClick = vi.fn()
// Act
render(<UpgradeBtn onClick={handleClick} loc="integration-test" />)