From 5549ab66ffb849e9c4ce6eee924f773d6a26caa1 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 25 Dec 2025 15:34:24 +0800 Subject: [PATCH] chore: some test (#30144) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../billing/billing-page/index.spec.tsx | 84 ++++++++ .../billing/header-billing-btn/index.spec.tsx | 92 ++++++++ .../billing/partner-stack/index.spec.tsx | 44 ++++ .../partner-stack/use-ps-info.spec.tsx | 197 ++++++++++++++++++ .../components/billing/plan/index.spec.tsx | 130 ++++++++++++ .../billing/progress-bar/index.spec.tsx | 25 +++ .../trigger-events-limit-modal/index.spec.tsx | 70 +++++++ .../billing/usage-info/apps-info.spec.tsx | 35 ++++ .../billing/usage-info/index.spec.tsx | 114 ++++++++++ .../billing/vector-space-full/index.spec.tsx | 58 ++++++ 10 files changed, 849 insertions(+) create mode 100644 web/app/components/billing/billing-page/index.spec.tsx create mode 100644 web/app/components/billing/header-billing-btn/index.spec.tsx create mode 100644 web/app/components/billing/partner-stack/index.spec.tsx create mode 100644 web/app/components/billing/partner-stack/use-ps-info.spec.tsx create mode 100644 web/app/components/billing/plan/index.spec.tsx create mode 100644 web/app/components/billing/progress-bar/index.spec.tsx create mode 100644 web/app/components/billing/trigger-events-limit-modal/index.spec.tsx create mode 100644 web/app/components/billing/usage-info/apps-info.spec.tsx create mode 100644 web/app/components/billing/usage-info/index.spec.tsx create mode 100644 web/app/components/billing/vector-space-full/index.spec.tsx diff --git a/web/app/components/billing/billing-page/index.spec.tsx b/web/app/components/billing/billing-page/index.spec.tsx new file mode 100644 index 0000000000..2310baa4f4 --- /dev/null +++ b/web/app/components/billing/billing-page/index.spec.tsx @@ -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 }) =>
, +})) + +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() + expect(screen.queryByRole('button', { name: /billing\.viewBillingTitle/ })).not.toBeInTheDocument() + + vi.clearAllMocks() + isManager = true + enableBilling = false + render() + 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() + + 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() + + const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) + expect(actionButton).toBeDisabled() + }) +}) diff --git a/web/app/components/billing/header-billing-btn/index.spec.tsx b/web/app/components/billing/header-billing-btn/index.spec.tsx new file mode 100644 index 0000000000..b87b733353 --- /dev/null +++ b/web/app/components/billing/header-billing-btn/index.spec.tsx @@ -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 +} + +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: () => , +})) + +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() + + expect(container.firstChild).toBeNull() + }) + + it('renders upgrade button for sandbox plan', () => { + ensureProviderContextMock().mockReturnValueOnce({ + plan: { + type: Plan.sandbox, + }, + enableBilling: true, + isFetchedPlan: true, + }) + + render() + + expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument() + }) + + it('renders plan badge and forwards clicks when not display-only', () => { + const onClick = vi.fn() + + const { rerender } = render() + + const badge = screen.getByText('pro').closest('div') + + expect(badge).toHaveClass('cursor-pointer') + + fireEvent.click(badge!) + expect(onClick).toHaveBeenCalledTimes(1) + + rerender() + expect(screen.getByText('pro').closest('div')).toHaveClass('cursor-default') + + fireEvent.click(screen.getByText('pro').closest('div')!) + expect(onClick).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/billing/partner-stack/index.spec.tsx b/web/app/components/billing/partner-stack/index.spec.tsx new file mode 100644 index 0000000000..7b4658cf0f --- /dev/null +++ b/web/app/components/billing/partner-stack/index.spec.tsx @@ -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() + + expect(saveOrUpdate).not.toHaveBeenCalled() + expect(bind).not.toHaveBeenCalled() + }) + + it('calls saveOrUpdate and bind once when running in cloud edition', () => { + render() + + expect(saveOrUpdate).toHaveBeenCalledTimes(1) + expect(bind).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/billing/partner-stack/use-ps-info.spec.tsx b/web/app/components/billing/partner-stack/use-ps-info.spec.tsx new file mode 100644 index 0000000000..14215f2514 --- /dev/null +++ b/web/app/components/billing/partner-stack/use-ps-info.spec.tsx @@ -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 = {} +const setSearchParams = (values: Record) => { + searchParamsValues = values +} + +type PartnerStackGlobal = typeof globalThis & { + __partnerStackCookieMocks?: { + get: ReturnType + set: ReturnType + remove: ReturnType + } + __partnerStackMutateAsync?: ReturnType +} + +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', + }) + }) +}) diff --git a/web/app/components/billing/plan/index.spec.tsx b/web/app/components/billing/plan/index.spec.tsx new file mode 100644 index 0000000000..bcdb83b5df --- /dev/null +++ b/web/app/components/billing/plan/index.spec.tsx @@ -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 => ( +
+ {props.isShow ? 'visible' : 'hidden'} +
+)) +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: () => , +})) + +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() + + 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() + + 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() + + expect(setShowAccountSettingModalMock).toHaveBeenCalledWith(null) + }) +}) diff --git a/web/app/components/billing/progress-bar/index.spec.tsx b/web/app/components/billing/progress-bar/index.spec.tsx new file mode 100644 index 0000000000..a9c91468de --- /dev/null +++ b/web/app/components/billing/progress-bar/index.spec.tsx @@ -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() + + 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() + + 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() + + expect(screen.getByTestId('billing-progress-bar')).toHaveClass('#2970FF') + }) +}) diff --git a/web/app/components/billing/trigger-events-limit-modal/index.spec.tsx b/web/app/components/billing/trigger-events-limit-modal/index.spec.tsx new file mode 100644 index 0000000000..a3d04c6031 --- /dev/null +++ b/web/app/components/billing/trigger-events-limit-modal/index.spec.tsx @@ -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 }) => ( +
+ {props.extraInfo} +
+)) + +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( + , + ) + + 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( + , + ) + + expect(planUpgradeModalMock).toHaveBeenCalled() + expect(screen.getByTestId('plan-upgrade-modal').getAttribute('data-show')).toBe('false') + }) +}) diff --git a/web/app/components/billing/usage-info/apps-info.spec.tsx b/web/app/components/billing/usage-info/apps-info.spec.tsx new file mode 100644 index 0000000000..7289b474e5 --- /dev/null +++ b/web/app/components/billing/usage-info/apps-info.spec.tsx @@ -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() + + 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() + }) +}) diff --git a/web/app/components/billing/usage-info/index.spec.tsx b/web/app/components/billing/usage-info/index.spec.tsx new file mode 100644 index 0000000000..3137c4865f --- /dev/null +++ b/web/app/components/billing/usage-info/index.spec.tsx @@ -0,0 +1,114 @@ +import { render, screen } from '@testing-library/react' +import { NUM_INFINITE } from '../config' +import UsageInfo from './index' + +const TestIcon = () => + +describe('UsageInfo', () => { + it('renders the metric with a suffix unit and tooltip text', () => { + render( + , + ) + + 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( + , + ) + + 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( + , + ) + + expect(screen.getByText(resetHint)).toBeInTheDocument() + expect(screen.queryByText('GB')).not.toBeInTheDocument() + }) + + it('displays unlimited text when total is infinite', () => { + render( + , + ) + + expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument() + }) + + it('applies warning color when usage is close to the limit', () => { + render( + , + ) + + 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( + , + ) + + 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( + , + ) + + expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/billing/vector-space-full/index.spec.tsx b/web/app/components/billing/vector-space-full/index.spec.tsx new file mode 100644 index 0000000000..de5607df41 --- /dev/null +++ b/web/app/components/billing/vector-space-full/index.spec.tsx @@ -0,0 +1,58 @@ +import { render, screen } from '@testing-library/react' +import VectorSpaceFull from './index' + +type VectorProviderGlobal = typeof globalThis & { + __vectorProviderContext?: ReturnType +} + +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: () => , +})) + +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() + + 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() + + expect(screen.getByText('8')).toBeInTheDocument() + expect(screen.getByText('10MB')).toBeInTheDocument() + }) +})