Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox

# Conflicts:
#	api/uv.lock
#	web/app/components/apps/__tests__/app-card.spec.tsx
#	web/app/components/apps/__tests__/list.spec.tsx
#	web/app/components/datasets/create/__tests__/index.spec.tsx
#	web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx
#	web/app/components/plugins/readme-panel/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/hooks/__tests__/index.spec.ts
#	web/eslint-suppressions.json
This commit is contained in:
yyh
2026-02-13 15:17:52 +08:00
898 changed files with 58772 additions and 34358 deletions

View File

@ -1,19 +1,26 @@
import type { SiteInfo } from '@/models/share'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import InfoModal from './info-modal'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import InfoModal from '../info-modal'
// Only mock react-i18next for translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
cleanup()
})
async function renderModal(ui: React.ReactElement) {
const result = render(ui)
await act(async () => {
vi.runAllTimers()
})
return result
}
describe('InfoModal', () => {
const mockOnClose = vi.fn()
@ -29,8 +36,8 @@ describe('InfoModal', () => {
})
describe('rendering', () => {
it('should not render when isShow is false', () => {
render(
it('should not render when isShow is false', async () => {
await renderModal(
<InfoModal
isShow={false}
onClose={mockOnClose}
@ -41,8 +48,8 @@ describe('InfoModal', () => {
expect(screen.queryByText('Test App')).not.toBeInTheDocument()
})
it('should render when isShow is true', () => {
render(
it('should render when isShow is true', async () => {
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -53,8 +60,8 @@ describe('InfoModal', () => {
expect(screen.getByText('Test App')).toBeInTheDocument()
})
it('should render app title', () => {
render(
it('should render app title', async () => {
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -65,13 +72,13 @@ describe('InfoModal', () => {
expect(screen.getByText('Test App')).toBeInTheDocument()
})
it('should render copyright when provided', () => {
it('should render copyright when provided', async () => {
const siteInfoWithCopyright: SiteInfo = {
...baseSiteInfo,
copyright: 'Dify Inc.',
}
render(
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -82,13 +89,13 @@ describe('InfoModal', () => {
expect(screen.getByText(/Dify Inc./)).toBeInTheDocument()
})
it('should render current year in copyright', () => {
it('should render current year in copyright', async () => {
const siteInfoWithCopyright: SiteInfo = {
...baseSiteInfo,
copyright: 'Test Company',
}
render(
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -100,13 +107,13 @@ describe('InfoModal', () => {
expect(screen.getByText(new RegExp(currentYear))).toBeInTheDocument()
})
it('should render custom disclaimer when provided', () => {
it('should render custom disclaimer when provided', async () => {
const siteInfoWithDisclaimer: SiteInfo = {
...baseSiteInfo,
custom_disclaimer: 'This is a custom disclaimer',
}
render(
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -117,8 +124,8 @@ describe('InfoModal', () => {
expect(screen.getByText('This is a custom disclaimer')).toBeInTheDocument()
})
it('should not render copyright section when not provided', () => {
render(
it('should not render copyright section when not provided', async () => {
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -130,8 +137,8 @@ describe('InfoModal', () => {
expect(screen.queryByText(new RegExp(`©.*${year}`))).not.toBeInTheDocument()
})
it('should render with undefined data', () => {
render(
it('should render with undefined data', async () => {
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -139,18 +146,17 @@ describe('InfoModal', () => {
/>,
)
// Modal should still render but without content
expect(screen.queryByText('Test App')).not.toBeInTheDocument()
})
it('should render with image icon type', () => {
it('should render with image icon type', async () => {
const siteInfoWithImage: SiteInfo = {
...baseSiteInfo,
icon_type: 'image',
icon_url: 'https://example.com/icon.png',
}
render(
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -163,8 +169,8 @@ describe('InfoModal', () => {
})
describe('close functionality', () => {
it('should call onClose when close button is clicked', () => {
render(
it('should call onClose when close button is clicked', async () => {
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}
@ -172,7 +178,6 @@ describe('InfoModal', () => {
/>,
)
// Find the close icon (RiCloseLine) which has text-text-tertiary class
const closeIcon = document.querySelector('[class*="text-text-tertiary"]')
expect(closeIcon).toBeInTheDocument()
if (closeIcon) {
@ -183,14 +188,14 @@ describe('InfoModal', () => {
})
describe('both copyright and disclaimer', () => {
it('should render both when both are provided', () => {
it('should render both when both are provided', async () => {
const siteInfoWithBoth: SiteInfo = {
...baseSiteInfo,
copyright: 'My Company',
custom_disclaimer: 'Disclaimer text here',
}
render(
await renderModal(
<InfoModal
isShow={true}
onClose={mockOnClose}

View File

@ -1,16 +1,8 @@
import type { SiteInfo } from '@/models/share'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import MenuDropdown from './menu-dropdown'
import MenuDropdown from '../menu-dropdown'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock next/navigation
const mockReplace = vi.fn()
const mockPathname = '/test-path'
vi.mock('next/navigation', () => ({
@ -20,7 +12,6 @@ vi.mock('next/navigation', () => ({
usePathname: () => mockPathname,
}))
// Mock web-app-context
const mockShareCode = 'test-share-code'
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: (selector: (state: Record<string, unknown>) => unknown) => {
@ -32,7 +23,6 @@ vi.mock('@/context/web-app-context', () => ({
},
}))
// Mock webapp-auth service
const mockWebAppLogout = vi.fn().mockResolvedValue(undefined)
vi.mock('@/service/webapp-auth', () => ({
webAppLogout: (...args: unknown[]) => mockWebAppLogout(...args),
@ -57,7 +47,6 @@ describe('MenuDropdown', () => {
it('should render the trigger button', () => {
render(<MenuDropdown data={baseSiteInfo} />)
// The trigger button contains a settings icon (RiEqualizer2Line)
const triggerButton = screen.getByRole('button')
expect(triggerButton).toBeInTheDocument()
})
@ -65,8 +54,7 @@ describe('MenuDropdown', () => {
it('should not show dropdown content initially', () => {
render(<MenuDropdown data={baseSiteInfo} />)
// Dropdown content should not be visible initially
expect(screen.queryByText('theme.theme')).not.toBeInTheDocument()
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
it('should show dropdown content when clicked', async () => {
@ -76,7 +64,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('theme.theme')).toBeInTheDocument()
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
})
@ -87,7 +75,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('userProfile.about')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.about')).toBeInTheDocument()
})
})
})
@ -105,7 +93,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('chat.privacyPolicyMiddle')).toBeInTheDocument()
expect(screen.getByText('share.chat.privacyPolicyMiddle')).toBeInTheDocument()
})
})
@ -116,7 +104,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('chat.privacyPolicyMiddle')).not.toBeInTheDocument()
expect(screen.queryByText('share.chat.privacyPolicyMiddle')).not.toBeInTheDocument()
})
})
@ -133,7 +121,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
const link = screen.getByText('chat.privacyPolicyMiddle').closest('a')
const link = screen.getByText('share.chat.privacyPolicyMiddle').closest('a')
expect(link).toHaveAttribute('href', privacyUrl)
expect(link).toHaveAttribute('target', '_blank')
})
@ -148,7 +136,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('userProfile.logout')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
})
})
@ -159,7 +147,7 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('userProfile.logout')).not.toBeInTheDocument()
expect(screen.queryByText('common.userProfile.logout')).not.toBeInTheDocument()
})
})
@ -170,10 +158,10 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('userProfile.logout')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
})
const logoutButton = screen.getByText('userProfile.logout')
const logoutButton = screen.getByText('common.userProfile.logout')
await act(async () => {
fireEvent.click(logoutButton)
})
@ -193,10 +181,10 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('userProfile.about')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.about')).toBeInTheDocument()
})
const aboutButton = screen.getByText('userProfile.about')
const aboutButton = screen.getByText('common.userProfile.about')
fireEvent.click(aboutButton)
await waitFor(() => {
@ -213,13 +201,13 @@ describe('MenuDropdown', () => {
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('theme.theme')).toBeInTheDocument()
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
rerender(<MenuDropdown data={baseSiteInfo} forceClose={true} />)
await waitFor(() => {
expect(screen.queryByText('theme.theme')).not.toBeInTheDocument()
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
})
})
@ -239,16 +227,14 @@ describe('MenuDropdown', () => {
const triggerButton = screen.getByRole('button')
// Open
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('theme.theme')).toBeInTheDocument()
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
// Close
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('theme.theme')).not.toBeInTheDocument()
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
})
})

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import NoData from './index'
import NoData from '../index'
describe('NoData', () => {
beforeEach(() => {

View File

@ -2,7 +2,7 @@ import type { Mock } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunBatch from './index'
import RunBatch from '../index'
vi.mock('@/hooks/use-breakpoints', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/hooks/use-breakpoints')>()
@ -15,14 +15,14 @@ vi.mock('@/hooks/use-breakpoints', async (importOriginal) => {
let latestOnParsed: ((data: string[][]) => void) | undefined
let receivedCSVDownloadProps: Record<string, unknown> | undefined
vi.mock('./csv-reader', () => ({
vi.mock('../csv-reader', () => ({
default: (props: { onParsed: (data: string[][]) => void }) => {
latestOnParsed = props.onParsed
return <div data-testid="csv-reader" />
},
}))
vi.mock('./csv-download', () => ({
vi.mock('../csv-download', () => ({
default: (props: { vars: { name: string }[] }) => {
receivedCSVDownloadProps = props
return <div data-testid="csv-download" />

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import CSVDownload from './index'
import CSVDownload from '../index'
const mockType = { Link: 'mock-link' }
let capturedProps: Record<string, unknown> | undefined

View File

@ -1,13 +1,20 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import CSVReader from './index'
import CSVReader from '../index'
let mockAcceptedFile: { name: string } | null = null
let capturedHandlers: Record<string, (payload: any) => void> = {}
type CSVReaderHandlers = {
onUploadAccepted?: (payload: { data: string[][] }) => void
onDragOver?: (event: DragEvent) => void
onDragLeave?: (event: DragEvent) => void
}
let capturedHandlers: CSVReaderHandlers = {}
vi.mock('react-papaparse', () => ({
useCSVReader: () => ({
CSVReader: ({ children, ...handlers }: any) => {
CSVReader: ({ children, ...handlers }: { children: (ctx: { getRootProps: () => Record<string, string>, acceptedFile: { name: string } | null }) => React.ReactNode } & CSVReaderHandlers) => {
capturedHandlers = handlers
return (
<div data-testid="csv-reader-wrapper">

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import ResDownload from './index'
import ResDownload from '../index'
const mockType = { Link: 'mock-link' }
let capturedProps: Record<string, unknown> | undefined

View File

@ -1,4 +1,4 @@
import type { InputValueTypes } from '../types'
import type { InputValueTypes } from '../../types'
import type { PromptConfig, PromptVariable } from '@/models/debug'
import type { SiteInfo } from '@/models/share'
import type { VisionFile, VisionSettings } from '@/types/app'
@ -6,7 +6,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Resolution, TransferMethod } from '@/types/app'
import RunOnce from './index'
import RunOnce from '../index'
vi.mock('@/hooks/use-breakpoints', () => {
const MediaType = {
@ -39,7 +39,6 @@ vi.mock('@/app/components/base/image-uploader/text-generation-image-uploader', (
}
})
// Mock FileUploaderInAttachmentWrapper as it requires context providers not available in tests
vi.mock('@/app/components/base/file-uploader', () => ({
FileUploaderInAttachmentWrapper: ({ value, onChange }: { value: object[], onChange: (files: object[]) => void }) => (
<div data-testid="file-uploader-mock">
@ -272,7 +271,6 @@ describe('RunOnce', () => {
selectInput: 'Option A',
})
})
// The Select component should be rendered
expect(screen.getByText('Select Input')).toBeInTheDocument()
})
})
@ -463,7 +461,6 @@ describe('RunOnce', () => {
key: 'textInput',
name: 'Text Input',
type: 'string',
// max_length is not set
}),
],
}