import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
import { act, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import dayjs from 'dayjs'
import * as React from 'react'
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
import { useModalContextSelector } from '@/context/modal-context'
import { ModalContextProvider } from '@/context/modal-context-provider'
import { createConsoleQueryWrapper, seedFeatures } from '@/test/console/query-data'
import { render } from '@/test/console/render'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
}),
useSearchParams: vi.fn(() => new URLSearchParams()),
}))
vi.mock('@/app/components/billing/pricing', () => ({
Pricing: () =>
billing.plansCommon.mostPopular
,
}))
vi.mock('@/app/components/plugins/update-plugin', () => ({
default: ({ onSave }: { onSave: () => void | Promise }) => (
),
}))
const mockConsoleStateReader = vi.fn()
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleStateReader())
})
const ModalBlockingState = () => {
const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen)
return
}
const UpdatePluginTrigger = ({
onSave,
category = PluginCategoryEnum.model,
}: {
onSave: () => void | Promise
category?: PluginCategoryEnum
}) => {
const setShowUpdatePluginModal = useModalContextSelector(
(state) => state.setShowUpdatePluginModal,
)
return (
)
}
const renderProvider = (
children: React.ReactNode = ,
features: Parameters[1] = {},
edition: DeploymentEdition = 'CLOUD',
) => {
const { wrapper: QueryWrapper, queryClient } = createConsoleQueryWrapper({
systemFeatures: { deployment_edition: edition },
})
seedFeatures(queryClient, features)
const { wrapper: NuqsWrapper } = createNuqsTestWrapper()
const wrapper = ({ children: wrapperChildren }: { children: React.ReactNode }) => (
{wrapperChildren}
)
return {
queryClient,
...render({children}, { wrapper }),
}
}
describe('ModalContextProvider trigger events limit modal', () => {
beforeEach(() => {
mockConsoleStateReader.mockReset()
window.localStorage.clear()
mockConsoleStateReader.mockReturnValue({
currentWorkspace: {
id: 'workspace-1',
},
})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('updates the visible quota and closes the modal when usage drops below the limit', async () => {
const features = {
billing: { subscription: { plan: 'professional' as const } },
trigger_event: { usage: 200, limit: 200, reset_date: dayjs().add(3, 'day').unix() },
}
const { queryClient } = renderProvider(undefined, features)
expect(await screen.findByRole('dialog')).toBeInTheDocument()
act(() => {
seedFeatures(queryClient, {
...features,
trigger_event: { ...features.trigger_event, usage: 250 },
})
})
expect(await screen.findByText('250')).toBeInTheDocument()
act(() => {
seedFeatures(queryClient, {
...features,
trigger_event: { ...features.trigger_event, usage: 100 },
})
})
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.getByText('clear')).toBeInTheDocument()
})
it.each(['COMMUNITY', 'ENTERPRISE'] as const)(
'does not show Cloud quota prompts in %s',
(edition) => {
renderProvider(
undefined,
{
billing: { subscription: { plan: 'sandbox' } },
trigger_event: { usage: 200, limit: 200 },
},
edition,
)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.getByText('clear')).toBeInTheDocument()
},
)
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
const features = {
billing: { subscription: { plan: 'professional' as const } },
trigger_event: { usage: 3000, limit: 3000, reset_date: dayjs().add(5, 'day').unix() },
}
// 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')
const user = userEvent.setup()
renderProvider(undefined, features)
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
expect(screen.getAllByText('3000')).toHaveLength(2)
expect(screen.getByText('blocked')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.getByText('clear')).toBeInTheDocument()
await waitFor(() => {
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
})
const [key, value] = (setItemSpy.mock.calls[0] ?? []) as [string, string]
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 features = {
billing: { subscription: { plan: 'professional' as const } },
trigger_event: { usage: 200, limit: 200, reset_date: dayjs().add(3, 'day').unix() },
}
vi.spyOn(localStorage, 'getItem').mockImplementation(() => {
throw new Error('Storage disabled')
})
const user = userEvent.setup()
const { rerender } = renderProvider(undefined, features)
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
rerender(
,
)
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.getByText('clear')).toBeInTheDocument()
})
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
const features = {
billing: { subscription: { plan: 'professional' as const } },
trigger_event: { usage: 120, limit: 120, reset_date: dayjs().add(2, 'day').unix() },
}
vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
throw new Error('Quota exceeded')
})
const user = userEvent.setup()
const { rerender } = renderProvider(undefined, features)
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
rerender(
,
)
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.getByText('clear')).toBeInTheDocument()
})
it('closes the trigger events limit modal and opens pricing when upgrading', async () => {
const features = {
billing: { subscription: { plan: 'professional' as const } },
trigger_event: { usage: 400, limit: 400, reset_date: dayjs().add(6, 'day').unix() },
}
const user = userEvent.setup()
renderProvider(undefined, features)
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
await waitFor(() =>
expect(screen.getByText('billing.plansCommon.mostPopular')).toBeInTheDocument(),
)
expect(screen.queryByText('400')).not.toBeInTheDocument()
expect(screen.getByText('blocked')).toBeInTheDocument()
})
})
describe('ModalContextProvider plugin update modal', () => {
beforeEach(() => {
mockConsoleStateReader.mockReset()
mockConsoleStateReader.mockReturnValue({
currentWorkspace: {
id: 'workspace-1',
},
})
})
it('keeps a model plugin update open until its refresh callback finishes', async () => {
let resolveSave: (() => void) | undefined
const onSave = vi.fn(
() =>
new Promise((resolve) => {
resolveSave = resolve
}),
)
const user = userEvent.setup()
renderProvider()
await user.click(screen.getByRole('button', { name: 'Open plugin update' }))
await user.click(screen.getByTestId('save-plugin-update'))
expect(onSave).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('save-plugin-update')).toBeInTheDocument()
resolveSave?.()
await waitFor(() => {
expect(screen.queryByTestId('save-plugin-update')).not.toBeInTheDocument()
})
})
it('closes a non-model plugin update immediately after saving', async () => {
const onSave = vi.fn()
const user = userEvent.setup()
renderProvider()
await user.click(screen.getByRole('button', { name: 'Open plugin update' }))
await user.click(screen.getByTestId('save-plugin-update'))
expect(onSave).toHaveBeenCalledTimes(1)
expect(screen.queryByTestId('save-plugin-update')).not.toBeInTheDocument()
})
})