mirror of
https://github.com/langgenius/dify.git
synced 2026-05-12 07:37:09 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: jyong <718720800@qq.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: hj24 <huangjian@dify.ai> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Ayush Baluni <73417844+aayushbaluni@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: jimcody1995 <jjimcody@gmail.com> Co-authored-by: James <63717587+jamesrayammons@users.noreply.github.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: jerryzai <jerryzh8710@protonmail.com> Co-authored-by: NVIDIAN <speedy.hpc@hotmail.com> Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: Junghwan <70629228+shaun0927@users.noreply.github.com> Co-authored-by: HeYinKazune <70251095+HeYin-OS@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Jingyi <jingyi.qi@dify.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
191 lines
5.7 KiB
TypeScript
191 lines
5.7 KiB
TypeScript
import { act, screen, waitFor } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import { defaultPlan } from '@/app/components/billing/config'
|
|
import { Plan } from '@/app/components/billing/type'
|
|
import { ModalContextProvider } from '@/context/modal-context-provider'
|
|
import { renderWithNuqs } from '@/test/nuqs-testing'
|
|
|
|
vi.mock('@/config', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/config')>()
|
|
return {
|
|
...actual,
|
|
IS_CLOUD_EDITION: true,
|
|
}
|
|
})
|
|
|
|
vi.mock('@/next/navigation', () => ({
|
|
useSearchParams: vi.fn(() => new URLSearchParams()),
|
|
}))
|
|
|
|
const mockUseProviderContext = vi.fn()
|
|
vi.mock('@/context/provider-context', () => ({
|
|
useProviderContext: () => mockUseProviderContext(),
|
|
}))
|
|
|
|
const mockUseAppContext = vi.fn()
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => mockUseAppContext(),
|
|
}))
|
|
|
|
let latestTriggerEventsModalProps: any = null
|
|
const triggerEventsLimitModalMock = vi.fn((props: any) => {
|
|
latestTriggerEventsModalProps = props
|
|
return (
|
|
<div data-testid="trigger-limit-modal">
|
|
<button type="button" onClick={props.onClose}>dismiss</button>
|
|
<button type="button" onClick={props.onUpgrade}>upgrade</button>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
vi.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
|
|
default: (props: any) => triggerEventsLimitModalMock(props),
|
|
}))
|
|
|
|
type DefaultPlanShape = typeof defaultPlan
|
|
type ResetShape = {
|
|
apiRateLimit: number | null
|
|
triggerEvents: number | null
|
|
}
|
|
type PlanShape = Omit<DefaultPlanShape, 'reset'> & { reset: ResetShape }
|
|
type PlanOverrides = Partial<Omit<DefaultPlanShape, 'usage' | 'total' | 'reset'>> & {
|
|
usage?: Partial<DefaultPlanShape['usage']>
|
|
total?: Partial<DefaultPlanShape['total']>
|
|
reset?: Partial<ResetShape>
|
|
}
|
|
|
|
const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
|
|
...defaultPlan,
|
|
...overrides,
|
|
usage: {
|
|
...defaultPlan.usage,
|
|
...overrides.usage,
|
|
},
|
|
total: {
|
|
...defaultPlan.total,
|
|
...overrides.total,
|
|
},
|
|
reset: {
|
|
...defaultPlan.reset,
|
|
...overrides.reset,
|
|
},
|
|
})
|
|
|
|
const renderProvider = () => renderWithNuqs(
|
|
<ModalContextProvider>
|
|
<div data-testid="modal-context-test-child" />
|
|
</ModalContextProvider>,
|
|
)
|
|
|
|
describe('ModalContextProvider trigger events limit modal', () => {
|
|
beforeEach(() => {
|
|
latestTriggerEventsModalProps = null
|
|
triggerEventsLimitModalMock.mockClear()
|
|
mockUseAppContext.mockReset()
|
|
mockUseProviderContext.mockReset()
|
|
window.localStorage.clear()
|
|
mockUseAppContext.mockReturnValue({
|
|
currentWorkspace: {
|
|
id: 'workspace-1',
|
|
},
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
|
|
const plan = createPlan({
|
|
type: Plan.professional,
|
|
usage: { triggerEvents: 3000 },
|
|
total: { triggerEvents: 3000 },
|
|
reset: { triggerEvents: 5 },
|
|
})
|
|
mockUseProviderContext.mockReturnValue({
|
|
plan,
|
|
isFetchedPlan: true,
|
|
})
|
|
// Note: vitest.setup.ts replaces localStorage with a mock object that has vi.fn() methods
|
|
// We need to spy on the mock's setItem, not Storage.prototype.setItem
|
|
const setItemSpy = vi.spyOn(localStorage, 'setItem')
|
|
|
|
renderProvider()
|
|
|
|
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
|
expect(latestTriggerEventsModalProps).toMatchObject({
|
|
usage: 3000,
|
|
total: 3000,
|
|
resetInDays: 5,
|
|
})
|
|
|
|
act(() => {
|
|
latestTriggerEventsModalProps.onClose()
|
|
})
|
|
|
|
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
|
await waitFor(() => {
|
|
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
|
|
})
|
|
const [key, value] = (setItemSpy.mock.calls[0] ?? []) as [any, any]
|
|
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
|
expect(value).toBe('1')
|
|
})
|
|
|
|
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
|
const plan = createPlan({
|
|
type: Plan.professional,
|
|
usage: { triggerEvents: 200 },
|
|
total: { triggerEvents: 200 },
|
|
reset: { triggerEvents: 3 },
|
|
})
|
|
mockUseProviderContext.mockReturnValue({
|
|
plan,
|
|
isFetchedPlan: true,
|
|
})
|
|
vi.spyOn(localStorage, 'getItem').mockImplementation(() => {
|
|
throw new Error('Storage disabled')
|
|
})
|
|
const setItemSpy = vi.spyOn(localStorage, 'setItem')
|
|
|
|
renderProvider()
|
|
|
|
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
|
|
|
act(() => {
|
|
latestTriggerEventsModalProps.onClose()
|
|
})
|
|
|
|
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
|
expect(setItemSpy).not.toHaveBeenCalled()
|
|
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
|
})
|
|
|
|
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
|
|
const plan = createPlan({
|
|
type: Plan.professional,
|
|
usage: { triggerEvents: 120 },
|
|
total: { triggerEvents: 120 },
|
|
reset: { triggerEvents: 2 },
|
|
})
|
|
mockUseProviderContext.mockReturnValue({
|
|
plan,
|
|
isFetchedPlan: true,
|
|
})
|
|
vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
|
|
throw new Error('Quota exceeded')
|
|
})
|
|
|
|
renderProvider()
|
|
|
|
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
|
|
|
act(() => {
|
|
latestTriggerEventsModalProps.onClose()
|
|
})
|
|
|
|
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
|
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
|
})
|
|
})
|