mirror of https://github.com/langgenius/dify.git
chore: some test (#30144)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
fb14644a79
commit
5549ab66ff
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import Billing from './index'
|
||||||
|
|
||||||
|
let currentBillingUrl: string | null = 'https://billing'
|
||||||
|
let fetching = false
|
||||||
|
let isManager = true
|
||||||
|
let enableBilling = true
|
||||||
|
|
||||||
|
const refetchMock = vi.fn()
|
||||||
|
const openAsyncWindowMock = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@/service/use-billing', () => ({
|
||||||
|
useBillingUrl: () => ({
|
||||||
|
data: currentBillingUrl,
|
||||||
|
isFetching: fetching,
|
||||||
|
refetch: refetchMock,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-async-window-open', () => ({
|
||||||
|
useAsyncWindowOpen: () => openAsyncWindowMock,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
isCurrentWorkspaceManager: isManager,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: () => ({
|
||||||
|
enableBilling,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../plan', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ loc }: { loc: string }) => <div data-testid="plan-component" data-loc={loc} />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Billing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
currentBillingUrl = 'https://billing'
|
||||||
|
fetching = false
|
||||||
|
isManager = true
|
||||||
|
enableBilling = true
|
||||||
|
refetchMock.mockResolvedValue({ data: 'https://billing' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the billing action when user is not manager or billing is disabled', () => {
|
||||||
|
isManager = false
|
||||||
|
render(<Billing />)
|
||||||
|
expect(screen.queryByRole('button', { name: /billing\.viewBillingTitle/ })).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
isManager = true
|
||||||
|
enableBilling = false
|
||||||
|
render(<Billing />)
|
||||||
|
expect(screen.queryByRole('button', { name: /billing\.viewBillingTitle/ })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens the billing window with the immediate url when the button is clicked', async () => {
|
||||||
|
render(<Billing />)
|
||||||
|
|
||||||
|
const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ })
|
||||||
|
fireEvent.click(actionButton)
|
||||||
|
|
||||||
|
await waitFor(() => expect(openAsyncWindowMock).toHaveBeenCalled())
|
||||||
|
const [, options] = openAsyncWindowMock.mock.calls[0]
|
||||||
|
expect(options).toMatchObject({
|
||||||
|
immediateUrl: currentBillingUrl,
|
||||||
|
features: 'noopener,noreferrer',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables the button while billing url is fetching', () => {
|
||||||
|
fetching = true
|
||||||
|
render(<Billing />)
|
||||||
|
|
||||||
|
const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ })
|
||||||
|
expect(actionButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { Plan } from '../type'
|
||||||
|
import HeaderBillingBtn from './index'
|
||||||
|
|
||||||
|
type HeaderGlobal = typeof globalThis & {
|
||||||
|
__mockProviderContext?: ReturnType<typeof vi.fn>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeaderGlobal(): HeaderGlobal {
|
||||||
|
return globalThis as HeaderGlobal
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureProviderContextMock = () => {
|
||||||
|
const globals = getHeaderGlobal()
|
||||||
|
if (!globals.__mockProviderContext)
|
||||||
|
throw new Error('Provider context mock not set')
|
||||||
|
return globals.__mockProviderContext
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => {
|
||||||
|
const mock = vi.fn()
|
||||||
|
const globals = getHeaderGlobal()
|
||||||
|
globals.__mockProviderContext = mock
|
||||||
|
return {
|
||||||
|
useProviderContext: () => mock(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('../upgrade-btn', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <button data-testid="upgrade-btn" type="button">Upgrade</button>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('HeaderBillingBtn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
ensureProviderContextMock().mockReturnValue({
|
||||||
|
plan: {
|
||||||
|
type: Plan.professional,
|
||||||
|
},
|
||||||
|
enableBilling: true,
|
||||||
|
isFetchedPlan: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders nothing when billing is disabled or plan is not fetched', () => {
|
||||||
|
ensureProviderContextMock().mockReturnValueOnce({
|
||||||
|
plan: {
|
||||||
|
type: Plan.professional,
|
||||||
|
},
|
||||||
|
enableBilling: false,
|
||||||
|
isFetchedPlan: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { container } = render(<HeaderBillingBtn />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders upgrade button for sandbox plan', () => {
|
||||||
|
ensureProviderContextMock().mockReturnValueOnce({
|
||||||
|
plan: {
|
||||||
|
type: Plan.sandbox,
|
||||||
|
},
|
||||||
|
enableBilling: true,
|
||||||
|
isFetchedPlan: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<HeaderBillingBtn />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders plan badge and forwards clicks when not display-only', () => {
|
||||||
|
const onClick = vi.fn()
|
||||||
|
|
||||||
|
const { rerender } = render(<HeaderBillingBtn onClick={onClick} />)
|
||||||
|
|
||||||
|
const badge = screen.getByText('pro').closest('div')
|
||||||
|
|
||||||
|
expect(badge).toHaveClass('cursor-pointer')
|
||||||
|
|
||||||
|
fireEvent.click(badge!)
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
rerender(<HeaderBillingBtn onClick={onClick} isDisplayOnly />)
|
||||||
|
expect(screen.getByText('pro').closest('div')).toHaveClass('cursor-default')
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('pro').closest('div')!)
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import PartnerStack from './index'
|
||||||
|
|
||||||
|
let isCloudEdition = true
|
||||||
|
|
||||||
|
const saveOrUpdate = vi.fn()
|
||||||
|
const bind = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@/config', () => ({
|
||||||
|
get IS_CLOUD_EDITION() {
|
||||||
|
return isCloudEdition
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('./use-ps-info', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => ({
|
||||||
|
saveOrUpdate,
|
||||||
|
bind,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('PartnerStack', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
isCloudEdition = true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not call partner stack helpers when not in cloud edition', () => {
|
||||||
|
isCloudEdition = false
|
||||||
|
|
||||||
|
render(<PartnerStack />)
|
||||||
|
|
||||||
|
expect(saveOrUpdate).not.toHaveBeenCalled()
|
||||||
|
expect(bind).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls saveOrUpdate and bind once when running in cloud edition', () => {
|
||||||
|
render(<PartnerStack />)
|
||||||
|
|
||||||
|
expect(saveOrUpdate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(bind).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { PARTNER_STACK_CONFIG } from '@/config'
|
||||||
|
import usePSInfo from './use-ps-info'
|
||||||
|
|
||||||
|
let searchParamsValues: Record<string, string | null> = {}
|
||||||
|
const setSearchParams = (values: Record<string, string | null>) => {
|
||||||
|
searchParamsValues = values
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartnerStackGlobal = typeof globalThis & {
|
||||||
|
__partnerStackCookieMocks?: {
|
||||||
|
get: ReturnType<typeof vi.fn>
|
||||||
|
set: ReturnType<typeof vi.fn>
|
||||||
|
remove: ReturnType<typeof vi.fn>
|
||||||
|
}
|
||||||
|
__partnerStackMutateAsync?: ReturnType<typeof vi.fn>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartnerStackGlobal(): PartnerStackGlobal {
|
||||||
|
return globalThis as PartnerStackGlobal
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureCookieMocks = () => {
|
||||||
|
const globals = getPartnerStackGlobal()
|
||||||
|
if (!globals.__partnerStackCookieMocks)
|
||||||
|
throw new Error('Cookie mocks not initialized')
|
||||||
|
return globals.__partnerStackCookieMocks
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureMutateAsync = () => {
|
||||||
|
const globals = getPartnerStackGlobal()
|
||||||
|
if (!globals.__partnerStackMutateAsync)
|
||||||
|
throw new Error('Mutate mock not initialized')
|
||||||
|
return globals.__partnerStackMutateAsync
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('js-cookie', () => {
|
||||||
|
const get = vi.fn()
|
||||||
|
const set = vi.fn()
|
||||||
|
const remove = vi.fn()
|
||||||
|
const globals = getPartnerStackGlobal()
|
||||||
|
globals.__partnerStackCookieMocks = { get, set, remove }
|
||||||
|
const cookieApi = { get, set, remove }
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: cookieApi,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useSearchParams: () => ({
|
||||||
|
get: (key: string) => searchParamsValues[key] ?? null,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
vi.mock('@/service/use-billing', () => {
|
||||||
|
const mutateAsync = vi.fn()
|
||||||
|
const globals = getPartnerStackGlobal()
|
||||||
|
globals.__partnerStackMutateAsync = mutateAsync
|
||||||
|
return {
|
||||||
|
useBindPartnerStackInfo: () => ({
|
||||||
|
mutateAsync,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usePSInfo', () => {
|
||||||
|
const originalLocationDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'location')
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.defineProperty(globalThis, 'location', {
|
||||||
|
value: { hostname: 'cloud.dify.ai' },
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setSearchParams({})
|
||||||
|
const { get, set, remove } = ensureCookieMocks()
|
||||||
|
get.mockReset()
|
||||||
|
set.mockReset()
|
||||||
|
remove.mockReset()
|
||||||
|
const mutate = ensureMutateAsync()
|
||||||
|
mutate.mockReset()
|
||||||
|
mutate.mockResolvedValue(undefined)
|
||||||
|
get.mockReturnValue('{}')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalLocationDescriptor)
|
||||||
|
Object.defineProperty(globalThis, 'location', originalLocationDescriptor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves partner info when query params change', () => {
|
||||||
|
const { get, set } = ensureCookieMocks()
|
||||||
|
get.mockReturnValue(JSON.stringify({ partnerKey: 'old', clickId: 'old-click' }))
|
||||||
|
setSearchParams({
|
||||||
|
ps_partner_key: 'new-partner',
|
||||||
|
ps_xid: 'new-click',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePSInfo())
|
||||||
|
|
||||||
|
expect(result.current.psPartnerKey).toBe('new-partner')
|
||||||
|
expect(result.current.psClickId).toBe('new-click')
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.saveOrUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(set).toHaveBeenCalledWith(
|
||||||
|
PARTNER_STACK_CONFIG.cookieName,
|
||||||
|
JSON.stringify({
|
||||||
|
partnerKey: 'new-partner',
|
||||||
|
clickId: 'new-click',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
expires: PARTNER_STACK_CONFIG.saveCookieDays,
|
||||||
|
path: '/',
|
||||||
|
domain: '.dify.ai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not overwrite cookie when params do not change', () => {
|
||||||
|
setSearchParams({
|
||||||
|
ps_partner_key: 'existing',
|
||||||
|
ps_xid: 'existing-click',
|
||||||
|
})
|
||||||
|
const { get } = ensureCookieMocks()
|
||||||
|
get.mockReturnValue(JSON.stringify({
|
||||||
|
partnerKey: 'existing',
|
||||||
|
clickId: 'existing-click',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePSInfo())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.saveOrUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { set } = ensureCookieMocks()
|
||||||
|
expect(set).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('binds partner info and clears cookie once', async () => {
|
||||||
|
setSearchParams({
|
||||||
|
ps_partner_key: 'bind-partner',
|
||||||
|
ps_xid: 'bind-click',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePSInfo())
|
||||||
|
|
||||||
|
const mutate = ensureMutateAsync()
|
||||||
|
const { remove } = ensureCookieMocks()
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.bind()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mutate).toHaveBeenCalledWith({
|
||||||
|
partnerKey: 'bind-partner',
|
||||||
|
clickId: 'bind-click',
|
||||||
|
})
|
||||||
|
expect(remove).toHaveBeenCalledWith(PARTNER_STACK_CONFIG.cookieName, {
|
||||||
|
path: '/',
|
||||||
|
domain: '.dify.ai',
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.bind()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mutate).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still removes cookie when bind fails with status 400', async () => {
|
||||||
|
const mutate = ensureMutateAsync()
|
||||||
|
mutate.mockRejectedValueOnce({ status: 400 })
|
||||||
|
setSearchParams({
|
||||||
|
ps_partner_key: 'bind-partner',
|
||||||
|
ps_xid: 'bind-click',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePSInfo())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.bind()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { remove } = ensureCookieMocks()
|
||||||
|
expect(remove).toHaveBeenCalledWith(PARTNER_STACK_CONFIG.cookieName, {
|
||||||
|
path: '/',
|
||||||
|
domain: '.dify.ai',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
|
||||||
|
import { Plan } from '../type'
|
||||||
|
import PlanComp from './index'
|
||||||
|
|
||||||
|
let currentPath = '/billing'
|
||||||
|
|
||||||
|
const push = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({ push }),
|
||||||
|
usePathname: () => currentPath,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const setShowAccountSettingModalMock = vi.fn()
|
||||||
|
vi.mock('@/context/modal-context', () => ({
|
||||||
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
useModalContextSelector: (selector: any) => selector({
|
||||||
|
setShowAccountSettingModal: setShowAccountSettingModalMock,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const providerContextMock = vi.fn()
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: () => providerContextMock(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
userProfile: { email: 'user@example.com' },
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mutateAsyncMock = vi.fn()
|
||||||
|
let isPending = false
|
||||||
|
vi.mock('@/service/use-education', () => ({
|
||||||
|
useEducationVerify: () => ({
|
||||||
|
mutateAsync: mutateAsyncMock,
|
||||||
|
isPending,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const verifyStateModalMock = vi.fn(props => (
|
||||||
|
<div data-testid="verify-modal" data-is-show={props.isShow ? 'true' : 'false'}>
|
||||||
|
{props.isShow ? 'visible' : 'hidden'}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
default: (props: any) => verifyStateModalMock(props),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../upgrade-btn', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <button data-testid="plan-upgrade-btn" type="button">Upgrade</button>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('PlanComp', () => {
|
||||||
|
const planMock = {
|
||||||
|
type: Plan.professional,
|
||||||
|
usage: {
|
||||||
|
teamMembers: 4,
|
||||||
|
documentsUploadQuota: 3,
|
||||||
|
vectorSpace: 8,
|
||||||
|
annotatedResponse: 5,
|
||||||
|
triggerEvents: 60,
|
||||||
|
apiRateLimit: 100,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
teamMembers: 10,
|
||||||
|
documentsUploadQuota: 20,
|
||||||
|
vectorSpace: 10,
|
||||||
|
annotatedResponse: 500,
|
||||||
|
triggerEvents: 100,
|
||||||
|
apiRateLimit: 200,
|
||||||
|
},
|
||||||
|
reset: {
|
||||||
|
triggerEvents: 2,
|
||||||
|
apiRateLimit: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
currentPath = '/billing'
|
||||||
|
isPending = false
|
||||||
|
providerContextMock.mockReturnValue({
|
||||||
|
plan: planMock,
|
||||||
|
enableEducationPlan: true,
|
||||||
|
allowRefreshEducationVerify: false,
|
||||||
|
isEducationAccount: false,
|
||||||
|
})
|
||||||
|
mutateAsyncMock.mockReset()
|
||||||
|
mutateAsyncMock.mockResolvedValue({ token: 'token' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders plan info and handles education verify success', async () => {
|
||||||
|
render(<PlanComp loc="billing-page" />)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.plans.professional.name')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('plan-upgrade-btn')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const verifyBtn = screen.getByText('education.toVerified')
|
||||||
|
fireEvent.click(verifyBtn)
|
||||||
|
|
||||||
|
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||||
|
await waitFor(() => expect(push).toHaveBeenCalledWith('/education-apply?token=token'))
|
||||||
|
expect(localStorage.removeItem).toHaveBeenCalledWith(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows modal when education verify fails', async () => {
|
||||||
|
mutateAsyncMock.mockRejectedValueOnce(new Error('boom'))
|
||||||
|
render(<PlanComp loc="billing-page" />)
|
||||||
|
|
||||||
|
const verifyBtn = screen.getByText('education.toVerified')
|
||||||
|
fireEvent.click(verifyBtn)
|
||||||
|
|
||||||
|
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||||
|
await waitFor(() => expect(screen.getByTestId('verify-modal').getAttribute('data-is-show')).toBe('true'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets modal context when on education apply path', () => {
|
||||||
|
currentPath = '/education-apply/setup'
|
||||||
|
render(<PlanComp loc="billing-page" />)
|
||||||
|
|
||||||
|
expect(setShowAccountSettingModalMock).toHaveBeenCalledWith(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import ProgressBar from './index'
|
||||||
|
|
||||||
|
describe('ProgressBar', () => {
|
||||||
|
it('renders with provided percent and color', () => {
|
||||||
|
render(<ProgressBar percent={42} color="bg-test-color" />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(bar).toHaveClass('bg-test-color')
|
||||||
|
expect(bar.getAttribute('style')).toContain('width: 42%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('caps width at 100% when percent exceeds max', () => {
|
||||||
|
render(<ProgressBar percent={150} color="bg-test-color" />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(bar.getAttribute('style')).toContain('width: 100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the default color when no color prop is provided', () => {
|
||||||
|
render(<ProgressBar percent={20} color={undefined as unknown as string} />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar')).toHaveClass('#2970FF')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import TriggerEventsLimitModal from './index'
|
||||||
|
|
||||||
|
const mockOnClose = vi.fn()
|
||||||
|
const mockOnUpgrade = vi.fn()
|
||||||
|
|
||||||
|
const planUpgradeModalMock = vi.fn((props: { show: boolean, title: string, description: string, extraInfo?: React.ReactNode, onClose: () => void, onUpgrade: () => void }) => (
|
||||||
|
<div
|
||||||
|
data-testid="plan-upgrade-modal"
|
||||||
|
data-show={props.show}
|
||||||
|
data-title={props.title}
|
||||||
|
data-description={props.description}
|
||||||
|
>
|
||||||
|
{props.extraInfo}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
default: (props: any) => planUpgradeModalMock(props),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('TriggerEventsLimitModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes the trigger usage props to the upgrade modal', () => {
|
||||||
|
render(
|
||||||
|
<TriggerEventsLimitModal
|
||||||
|
show
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onUpgrade={mockOnUpgrade}
|
||||||
|
usage={12}
|
||||||
|
total={20}
|
||||||
|
resetInDays={5}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const modal = screen.getByTestId('plan-upgrade-modal')
|
||||||
|
expect(modal.getAttribute('data-show')).toBe('true')
|
||||||
|
expect(modal.getAttribute('data-title')).toContain('billing.triggerLimitModal.title')
|
||||||
|
expect(modal.getAttribute('data-description')).toContain('billing.triggerLimitModal.description')
|
||||||
|
expect(planUpgradeModalMock).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const passedProps = planUpgradeModalMock.mock.calls[0][0]
|
||||||
|
expect(passedProps.onClose).toBe(mockOnClose)
|
||||||
|
expect(passedProps.onUpgrade).toBe(mockOnUpgrade)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.triggerLimitModal.usageTitle')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('12')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('20')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders even when trigger modal is hidden', () => {
|
||||||
|
render(
|
||||||
|
<TriggerEventsLimitModal
|
||||||
|
show={false}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onUpgrade={mockOnUpgrade}
|
||||||
|
usage={0}
|
||||||
|
total={0}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(planUpgradeModalMock).toHaveBeenCalled()
|
||||||
|
expect(screen.getByTestId('plan-upgrade-modal').getAttribute('data-show')).toBe('false')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { defaultPlan } from '../config'
|
||||||
|
import AppsInfo from './apps-info'
|
||||||
|
|
||||||
|
const appsUsage = 7
|
||||||
|
const appsTotal = 15
|
||||||
|
|
||||||
|
const mockPlan = {
|
||||||
|
...defaultPlan,
|
||||||
|
usage: {
|
||||||
|
...defaultPlan.usage,
|
||||||
|
buildApps: appsUsage,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
...defaultPlan.total,
|
||||||
|
buildApps: appsTotal,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: () => ({
|
||||||
|
plan: mockPlan,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('AppsInfo', () => {
|
||||||
|
it('renders build apps usage information with context data', () => {
|
||||||
|
render(<AppsInfo className="apps-info-class" />)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.usagePage.buildApps')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(`${appsUsage}`)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(`${appsTotal}`)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('billing.usagePage.buildApps').closest('.apps-info-class')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { NUM_INFINITE } from '../config'
|
||||||
|
import UsageInfo from './index'
|
||||||
|
|
||||||
|
const TestIcon = () => <span data-testid="usage-icon" />
|
||||||
|
|
||||||
|
describe('UsageInfo', () => {
|
||||||
|
it('renders the metric with a suffix unit and tooltip text', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Apps"
|
||||||
|
usage={30}
|
||||||
|
total={100}
|
||||||
|
unit="GB"
|
||||||
|
tooltip="tooltip text"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('usage-icon')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Apps')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('30')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('GB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders inline unit when unitPosition is inline', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={20}
|
||||||
|
total={100}
|
||||||
|
unit="GB"
|
||||||
|
unitPosition="inline"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('100GB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows reset hint text instead of the unit when resetHint is provided', () => {
|
||||||
|
const resetHint = 'Resets in 3 days'
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={20}
|
||||||
|
total={100}
|
||||||
|
unit="GB"
|
||||||
|
resetHint={resetHint}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText(resetHint)).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('GB')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays unlimited text when total is infinite', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={10}
|
||||||
|
total={NUM_INFINITE}
|
||||||
|
unit="GB"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies warning color when usage is close to the limit', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={85}
|
||||||
|
total={100}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-warning-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies error color when usage exceeds the limit', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={120}
|
||||||
|
total={100}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render the icon when hideIcon is true', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={5}
|
||||||
|
total={100}
|
||||||
|
hideIcon
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import VectorSpaceFull from './index'
|
||||||
|
|
||||||
|
type VectorProviderGlobal = typeof globalThis & {
|
||||||
|
__vectorProviderContext?: ReturnType<typeof vi.fn>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVectorGlobal(): VectorProviderGlobal {
|
||||||
|
return globalThis as VectorProviderGlobal
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => {
|
||||||
|
const mock = vi.fn()
|
||||||
|
getVectorGlobal().__vectorProviderContext = mock
|
||||||
|
return {
|
||||||
|
useProviderContext: () => mock(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('../upgrade-btn', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <button data-testid="vector-upgrade-btn" type="button">Upgrade</button>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('VectorSpaceFull', () => {
|
||||||
|
const planMock = {
|
||||||
|
type: 'team',
|
||||||
|
usage: {
|
||||||
|
vectorSpace: 8,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
vectorSpace: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
const globals = getVectorGlobal()
|
||||||
|
globals.__vectorProviderContext?.mockReturnValue({
|
||||||
|
plan: planMock,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tip text and upgrade button', () => {
|
||||||
|
render(<VectorSpaceFull />)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.vectorSpace.fullTip')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('billing.vectorSpace.fullSolution')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('vector-upgrade-btn')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows vector usage and total', () => {
|
||||||
|
render(<VectorSpaceFull />)
|
||||||
|
|
||||||
|
expect(screen.getByText('8')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('10MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue