mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
316 lines
8.7 KiB
TypeScript
316 lines
8.7 KiB
TypeScript
import type { ReactElement } from 'react'
|
|
import { act, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { consoleQuery } from '@/service/client'
|
|
import {
|
|
createConsoleQueryClient,
|
|
renderWithConsoleQuery,
|
|
seedFeatures,
|
|
} from '@/test/console/query-data'
|
|
import DuplicateAppModal from '../index'
|
|
|
|
const { mockAppQuota, toastErrorMock } = vi.hoisted(() => ({
|
|
mockAppQuota: { size: 0, limit: 1 },
|
|
toastErrorMock: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
|
toast: {
|
|
error: (...args: unknown[]) => toastErrorMock(...args),
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/base/app-icon', () => ({
|
|
default: () => <span>app-icon</span>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/billing/apps-full-in-dialog', () => ({
|
|
default: () => <div>apps-full</div>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
|
default: ({
|
|
onOpenChange,
|
|
onSelect,
|
|
}: {
|
|
onOpenChange: (open: boolean) => void
|
|
onSelect: (payload: { type: 'emoji'; icon: string; background: string }) => void
|
|
}) => {
|
|
let selectedBackground = '#FFEAD5'
|
|
return (
|
|
<div>
|
|
<input placeholder="Search emojis..." />
|
|
<button type="button" onClick={() => {}}>
|
|
<em-emoji />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="#E4FBCC"
|
|
onClick={() => {
|
|
selectedBackground = '#E4FBCC'
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onSelect({ type: 'emoji', icon: '🤖', background: selectedBackground })
|
|
onOpenChange(false)
|
|
}}
|
|
>
|
|
iconPicker.ok
|
|
</button>
|
|
<button type="button" onClick={() => onOpenChange(false)}>
|
|
iconPicker.cancel
|
|
</button>
|
|
</div>
|
|
)
|
|
},
|
|
}))
|
|
|
|
function render(ui: ReactElement) {
|
|
return renderWithConsoleQuery(ui, {
|
|
systemFeatures: { deployment_edition: 'CLOUD' },
|
|
features: { apps: mockAppQuota },
|
|
})
|
|
}
|
|
|
|
describe('DuplicateAppModal', () => {
|
|
const getIconButton = () =>
|
|
screen.getByRole('button', {
|
|
name: /operation\.edit.*appCustomize\.subTitle/,
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockAppQuota.size = 0
|
|
mockAppQuota.limit = 1
|
|
})
|
|
|
|
it('should render a named dialog', () => {
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Demo App"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
show
|
|
onConfirm={vi.fn()}
|
|
onHide={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByRole('dialog', { name: /duplicateTitle/ })).toBeInTheDocument()
|
|
})
|
|
|
|
it('should validate the name before duplicating and update the input value', async () => {
|
|
const onConfirm = vi.fn()
|
|
const onHide = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName=" "
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
icon_background="#FFEAD5"
|
|
show
|
|
onConfirm={onConfirm}
|
|
onHide={onHide}
|
|
/>,
|
|
)
|
|
|
|
const input = screen.getByRole('textbox', { name: /appCustomize\.subTitle/ })
|
|
await user.clear(input)
|
|
await user.type(input, 'Updated App')
|
|
expect(input).toHaveValue('Updated App')
|
|
|
|
await user.clear(input)
|
|
await user.click(screen.getByRole('textbox', { name: /appCustomize\.subTitle/ }))
|
|
await user.keyboard('{Enter}')
|
|
|
|
expect(toastErrorMock).toHaveBeenCalledWith(
|
|
expect.stringMatching(/(?:^|\.)appCustomize\.nameRequired(?=$|:)/),
|
|
)
|
|
expect(onConfirm).not.toHaveBeenCalled()
|
|
expect(onHide).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should update the selected icon before confirming the duplicate', async () => {
|
|
const onConfirm = vi.fn()
|
|
const onHide = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Demo App"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
icon_background="#FFEAD5"
|
|
show
|
|
onConfirm={onConfirm}
|
|
onHide={onHide}
|
|
/>,
|
|
)
|
|
|
|
await user.click(getIconButton())
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
|
})
|
|
await user.click(screen.getByRole('button', { name: '#E4FBCC', hidden: true }))
|
|
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/, hidden: true }))
|
|
await waitFor(() => {
|
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
|
})
|
|
await user.click(screen.getByRole('button', { name: /(?:^|\.)duplicate(?=$|:)/ }))
|
|
|
|
expect(onConfirm).toHaveBeenCalledWith({
|
|
name: 'Demo App',
|
|
icon_type: 'emoji',
|
|
icon: '🤖',
|
|
icon_background: '#E4FBCC',
|
|
})
|
|
expect(onHide).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onHide when close button is clicked', async () => {
|
|
const onHide = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Demo App"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
icon_background="#FFEAD5"
|
|
show
|
|
onConfirm={vi.fn()}
|
|
onHide={onHide}
|
|
/>,
|
|
)
|
|
|
|
await user.click(screen.getByRole('button', { name: /(?:^|\.)operation\.close(?=$|:)/ }))
|
|
|
|
expect(onHide).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onHide when Escape is pressed', async () => {
|
|
const onHide = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Demo App"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
show
|
|
onConfirm={vi.fn()}
|
|
onHide={onHide}
|
|
/>,
|
|
)
|
|
|
|
await user.keyboard('{Escape}')
|
|
|
|
expect(onHide).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should not submit with Enter when the app limit is reached', async () => {
|
|
const onConfirm = vi.fn()
|
|
const onHide = vi.fn()
|
|
const user = userEvent.setup()
|
|
mockAppQuota.size = 1
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Demo App"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
show
|
|
onConfirm={onConfirm}
|
|
onHide={onHide}
|
|
/>,
|
|
)
|
|
|
|
await user.click(screen.getByRole('textbox', { name: /appCustomize\.subTitle/ }))
|
|
await user.keyboard('{Enter}')
|
|
|
|
expect(onConfirm).not.toHaveBeenCalled()
|
|
expect(onHide).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should preserve the current image icon when the picker closes without selecting', async () => {
|
|
const onConfirm = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<DuplicateAppModal
|
|
appName="Image App"
|
|
icon_type="image"
|
|
icon="original-file"
|
|
icon_url="https://example.com/original.png"
|
|
show
|
|
onConfirm={onConfirm}
|
|
onHide={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
await user.click(getIconButton())
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
|
})
|
|
const emojiButton = document.querySelector('em-emoji')?.closest('button')
|
|
expect(emojiButton).toBeTruthy()
|
|
await user.click(emojiButton!)
|
|
await user.click(screen.getByRole('button', { name: '#E4FBCC', hidden: true }))
|
|
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/, hidden: true }))
|
|
await waitFor(() => {
|
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
|
})
|
|
await user.click(getIconButton())
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
|
})
|
|
await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/, hidden: true }))
|
|
await waitFor(() => {
|
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
|
})
|
|
await user.click(screen.getByRole('button', { name: /(?:^|\.)duplicate(?=$|:)/ }))
|
|
|
|
expect(onConfirm).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
name: 'Image App',
|
|
icon_type: 'emoji',
|
|
icon: expect.any(String),
|
|
icon_background: '#E4FBCC',
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('waits for the real Cloud quota and allows an unlimited quota without an upgrade notice', async () => {
|
|
const queryClient = createConsoleQueryClient()
|
|
void queryClient.query({
|
|
...consoleQuery.features.get.queryOptions(),
|
|
queryFn: () => new Promise(() => {}),
|
|
})
|
|
const onConfirm = vi.fn()
|
|
renderWithConsoleQuery(
|
|
<DuplicateAppModal
|
|
appName="Existing"
|
|
icon_type="emoji"
|
|
icon="🤖"
|
|
show
|
|
onConfirm={onConfirm}
|
|
onHide={vi.fn()}
|
|
/>,
|
|
{ queryClient, systemFeatures: { deployment_edition: 'CLOUD' } },
|
|
)
|
|
const button = screen.getByRole('button', { name: /(?:^|\.)duplicate(?=$|:)/ })
|
|
expect(button).toBeDisabled()
|
|
expect(screen.queryByText('apps-full')).not.toBeInTheDocument()
|
|
await act(async () => {
|
|
seedFeatures(queryClient, { apps: { size: 100, limit: 0 } })
|
|
})
|
|
await waitFor(() => expect(button).toBeEnabled())
|
|
await userEvent.setup().click(button)
|
|
expect(onConfirm).toHaveBeenCalledOnce()
|
|
})
|