diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 4b2ad6864dc..0ee697c927c 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -474,11 +474,6 @@
"count": 1
}
},
- "web/app/components/app/configuration/base/var-highlight/index.tsx": {
- "react-refresh/only-export-components": {
- "count": 1
- }
- },
"web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@@ -1063,34 +1058,6 @@
"count": 1
}
},
- "web/app/components/base/block-input/index.stories.tsx": {
- "no-console": {
- "count": 2
- },
- "ts/no-explicit-any": {
- "count": 1
- }
- },
- "web/app/components/base/block-input/index.tsx": {
- "jsx-a11y/click-events-have-key-events": {
- "count": 1
- },
- "jsx-a11y/no-static-element-interactions": {
- "count": 1
- },
- "react-refresh/only-export-components": {
- "count": 1
- },
- "react/no-nested-component-definitions": {
- "count": 1
- },
- "react/set-state-in-effect": {
- "count": 1
- },
- "react/static-components": {
- "count": 2
- }
- },
"web/app/components/base/carousel/index.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
@@ -1382,7 +1349,7 @@
},
"web/app/components/base/error-boundary/index.tsx": {
"react-refresh/only-export-components": {
- "count": 3
+ "count": 1
},
"react/jsx-no-key-after-spread": {
"count": 1
@@ -2605,11 +2572,6 @@
"count": 3
}
},
- "web/app/components/datasets/common/retrieval-method-info/index.tsx": {
- "react-refresh/only-export-components": {
- "count": 1
- }
- },
"web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@@ -3084,19 +3046,6 @@
"count": 1
}
},
- "web/app/components/datasets/documents/detail/metadata/components/metadata-field-list.tsx": {
- "ts/no-non-null-asserted-optional-chain": {
- "count": 1
- }
- },
- "web/app/components/datasets/documents/detail/metadata/hooks/use-metadata-state.ts": {
- "react-hooks-extra/no-direct-set-state-in-use-effect": {
- "count": 6
- },
- "react/set-state-in-effect": {
- "count": 6
- }
- },
"web/app/components/datasets/documents/detail/metadata/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 1
@@ -5116,10 +5065,10 @@
},
"web/app/components/workflow/nodes/_base/components/layout/index.tsx": {
"no-barrel-files/no-barrel-files": {
- "count": 7
+ "count": 6
},
"react-refresh/only-export-components": {
- "count": 7
+ "count": 6
}
},
"web/app/components/workflow/nodes/_base/components/mcp-tool-availability.tsx": {
@@ -5481,9 +5430,6 @@
"erasable-syntax-only/enums": {
"count": 1
},
- "no-barrel-files/no-barrel-files": {
- "count": 1
- },
"ts/no-explicit-any": {
"count": 1
}
@@ -7172,11 +7118,6 @@
"count": 1
}
},
- "web/service/__tests__/use-snippet-workflows.spec.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/service/__tests__/use-tools.spec.tsx": {
"no-restricted-imports": {
"count": 1
@@ -7281,7 +7222,7 @@
"count": 1
},
"ts/no-explicit-any": {
- "count": 26
+ "count": 13
}
},
"web/service/datasets.ts": {
@@ -7289,7 +7230,7 @@
"count": 1
},
"ts/no-explicit-any": {
- "count": 6
+ "count": 5
}
},
"web/service/debug.ts": {
diff --git a/packages/dify-ui/src/switch/__tests__/index.spec.tsx b/packages/dify-ui/src/switch/__tests__/index.spec.tsx
index 28aa8a655ce..0e289539581 100644
--- a/packages/dify-ui/src/switch/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/switch/__tests__/index.spec.tsx
@@ -123,9 +123,13 @@ describe('Switch', () => {
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
})
- it('should have focus-visible ring-3 styles', async () => {
+ it('should replace the native focus outline with the accent focus ring', async () => {
const screen = await render()
- await expect.element(screen.getByRole('switch')).toHaveClass('focus-visible:ring-2')
+ await expect.element(screen.getByRole('switch')).toHaveClass(
+ 'outline-hidden',
+ 'focus-visible:ring-2',
+ 'focus-visible:ring-state-accent-solid',
+ )
})
it('should respect prefers-reduced-motion', async () => {
diff --git a/packages/dify-ui/src/switch/index.tsx b/packages/dify-ui/src/switch/index.tsx
index d35809ecad8..768e009488e 100644
--- a/packages/dify-ui/src/switch/index.tsx
+++ b/packages/dify-ui/src/switch/index.tsx
@@ -10,7 +10,7 @@ import { cn } from '../cn'
const switchRootStateClassName = 'bg-components-toggle-bg-unchecked hover:bg-components-toggle-bg-unchecked-hover data-checked:bg-components-toggle-bg data-checked:hover:bg-components-toggle-bg-hover data-disabled:cursor-not-allowed data-disabled:bg-components-toggle-bg-unchecked-disabled data-disabled:hover:bg-components-toggle-bg-unchecked-disabled data-disabled:data-checked:bg-components-toggle-bg-disabled data-disabled:data-checked:hover:bg-components-toggle-bg-disabled'
const switchRootVariants = cva(
- `group relative inline-flex shrink-0 cursor-pointer touch-manipulation items-center transition-colors duration-200 ease-in-out focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none ${switchRootStateClassName}`,
+ `group relative inline-flex shrink-0 cursor-pointer touch-manipulation items-center outline-hidden transition-colors duration-200 ease-in-out focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none ${switchRootStateClassName}`,
{
variants: {
size: {
diff --git a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts
deleted file mode 100644
index cc97065d8f8..00000000000
--- a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Integration test: DSL export/import flow
- *
- * Validates DSL export logic (sync draft → check secrets → download)
- * and DSL import modal state management.
- */
-import { act, renderHook } from '@testing-library/react'
-import { describe, expect, it, vi } from 'vitest'
-
-const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined)
-const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' })
-const mockNotify = vi.fn()
-const mockToast = {
- success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }),
- error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }),
- warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }),
- info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }),
- dismiss: vi.fn(),
- update: vi.fn(),
- promise: vi.fn(),
-}
-
-vi.mock('@langgenius/dify-ui/toast', () => ({
- toast: mockToast,
-}))
-const mockEventEmitter = { emit: vi.fn() }
-const mockDownloadBlob = vi.fn()
-
-vi.mock('react-i18next', () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- }),
-}))
-
-vi.mock('@/app/components/workflow/constants', () => ({
- DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
-}))
-
-vi.mock('@/app/components/workflow/store', () => ({
- useWorkflowStore: () => ({
- getState: () => ({
- pipelineId: 'pipeline-abc',
- knowledgeName: 'My Pipeline',
- }),
- }),
-}))
-
-vi.mock('@/context/event-emitter', () => ({
- useEventEmitterContextContext: () => ({
- eventEmitter: mockEventEmitter,
- }),
-}))
-
-vi.mock('@/service/use-pipeline', () => ({
- useExportPipelineDSL: () => ({
- mutateAsync: mockExportPipelineConfig,
- }),
-}))
-
-vi.mock('@/service/workflow', () => ({
- fetchWorkflowDraft: vi.fn(),
-}))
-
-vi.mock('@/utils/download', () => ({
- downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
-}))
-
-vi.mock('@/app/components/rag-pipeline/hooks/use-nodes-sync-draft', () => ({
- useNodesSyncDraft: () => ({
- doSyncWorkflowDraft: mockDoSyncWorkflowDraft,
- }),
-}))
-
-describe('DSL Export/Import Flow', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- describe('Export Flow', () => {
- it('should sync draft then export then download', async () => {
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.handleExportDSL()
- })
-
- expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
- expect(mockExportPipelineConfig).toHaveBeenCalledWith({
- pipelineId: 'pipeline-abc',
- include: false,
- })
- expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
- fileName: 'My Pipeline.pipeline',
- }))
- })
-
- it('should export with include flag when specified', async () => {
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.handleExportDSL(true)
- })
-
- expect(mockExportPipelineConfig).toHaveBeenCalledWith({
- pipelineId: 'pipeline-abc',
- include: true,
- })
- })
-
- it('should notify on export error', async () => {
- mockDoSyncWorkflowDraft.mockRejectedValueOnce(new Error('sync failed'))
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.handleExportDSL()
- })
-
- expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
- type: 'error',
- }))
- })
- })
-
- describe('Export Check Flow', () => {
- it('should export directly when no secret environment variables', async () => {
- const { fetchWorkflowDraft } = await import('@/service/workflow')
- vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({
- environment_variables: [
- { value_type: 'string', key: 'API_URL', value: 'https://api.example.com' },
- ],
- } as unknown as Awaited>)
-
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.exportCheck()
- })
-
- // Should proceed to export directly (no secret vars)
- expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
- })
-
- it('should emit DSL_EXPORT_CHECK event when secret variables exist', async () => {
- const { fetchWorkflowDraft } = await import('@/service/workflow')
- vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({
- environment_variables: [
- { value_type: 'secret', key: 'API_KEY', value: '***' },
- ],
- } as unknown as Awaited>)
-
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.exportCheck()
- })
-
- expect(mockEventEmitter.emit).toHaveBeenCalledWith(expect.objectContaining({
- type: 'DSL_EXPORT_CHECK',
- payload: expect.objectContaining({
- data: expect.arrayContaining([
- expect.objectContaining({ value_type: 'secret' }),
- ]),
- }),
- }))
- })
-
- it('should notify on export check error', async () => {
- const { fetchWorkflowDraft } = await import('@/service/workflow')
- vi.mocked(fetchWorkflowDraft).mockRejectedValueOnce(new Error('fetch failed'))
-
- const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
- const { result } = renderHook(() => useDSL())
-
- await act(async () => {
- await result.current.exportCheck()
- })
-
- expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
- type: 'error',
- }))
- })
- })
-})
diff --git a/web/__tests__/xss-prevention.test.tsx b/web/__tests__/xss-prevention.test.tsx
deleted file mode 100644
index 233cbebf0ee..00000000000
--- a/web/__tests__/xss-prevention.test.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * XSS Prevention Test Suite
- *
- * This test verifies that the XSS vulnerability in block-input has been properly
- * fixed by replacing dangerouslySetInnerHTML with safe React rendering.
- */
-
-import { cleanup, render } from '@testing-library/react'
-import * as React from 'react'
-import BlockInput from '../app/components/base/block-input'
-
-// Mock styles
-vi.mock('../app/components/app/configuration/base/var-highlight/style.module.css', () => ({
- default: {
- item: 'mock-item-class',
- },
-}))
-
-describe('XSS Prevention - Block Input Security', () => {
- afterEach(() => {
- cleanup()
- })
-
- describe('BlockInput Component Security', () => {
- it('should safely render malicious variable names without executing scripts', () => {
- const testInput = 'user@test.com{{}}'
- const { container } = render()
-
- const scriptElements = container.querySelectorAll('script')
- expect(scriptElements).toHaveLength(0)
-
- const textContent = container.textContent
- expect(textContent).toContain(''}
- const { container } = render()
-
- const spanElement = container.querySelector('span')
- const scriptElements = container.querySelectorAll('script')
-
- expect(spanElement?.textContent).toBe('')
- expect(scriptElements).toHaveLength(0)
- })
- })
-})
-
-export {}
diff --git a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx
deleted file mode 100644
index 573cdf02334..00000000000
--- a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import type { App, AppSSO } from '@/types/app'
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import * as React from 'react'
-import { AppModeEnum } from '@/types/app'
-import AppInfo from '..'
-
-const mockDetailPanel = vi.hoisted(() => vi.fn())
-const mockModals = vi.hoisted(() => vi.fn())
-
-let mockAppPermissionKeys = ['app.acl.view_layout']
-const mockSetPanelOpen = vi.fn()
-
-vi.mock('@/context/app-context', () => ({
- useAppContext: () => ({
- workspacePermissionKeys: ['app.create_and_management'],
- }),
- useSelector: (selector: (state: { userProfile: { id: string }, workspacePermissionKeys: string[] }) => unknown) => selector({
- userProfile: { id: 'user-1' },
- workspacePermissionKeys: ['app.create_and_management'],
- }),
-}))
-
-vi.mock('../app-info-trigger', () => ({
- default: React.memo(({ appDetail, expand, onClick }: {
- appDetail: App & Partial
- expand: boolean
- onClick: () => void
- }) => (
-
- )),
-}))
-
-vi.mock('../app-info-detail-panel', () => ({
- default: React.memo((props: { show: boolean, onClose: () => void }) => {
- mockDetailPanel(props)
- return props.show ? : null
- }),
-}))
-
-vi.mock('../app-info-modals', () => ({
- default: React.memo((props: { activeModal: string | null }) => {
- mockModals(props)
- return props.activeModal ? : null
- }),
-}))
-
-const mockAppDetail: App & Partial = {
- id: 'app-1',
- name: 'Test App',
- mode: AppModeEnum.CHAT,
- icon: '🤖',
- icon_type: 'emoji',
- icon_background: '#FFEAD5',
- icon_url: '',
- description: '',
- use_icon_as_answer_icon: false,
- permission_keys: mockAppPermissionKeys,
-} as App & Partial
-
-const mockUseAppInfoActions = {
- appDetail: mockAppDetail,
- panelOpen: false,
- setPanelOpen: mockSetPanelOpen,
- closePanel: vi.fn(),
- activeModal: null as string | null,
- openModal: vi.fn(),
- closeModal: vi.fn(),
- secretEnvList: [],
- setSecretEnvList: vi.fn(),
- onEdit: vi.fn(),
- onCopy: vi.fn(),
- onExport: vi.fn(),
- exportCheck: vi.fn(),
- handleConfirmExport: vi.fn(),
- onConfirmDelete: vi.fn(),
-}
-
-vi.mock('../use-app-info-actions', () => ({
- useAppInfoActions: () => mockUseAppInfoActions,
-}))
-
-describe('AppInfo', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mockAppPermissionKeys = ['app.acl.view_layout']
- mockUseAppInfoActions.appDetail = mockAppDetail
- mockUseAppInfoActions.appDetail.permission_keys = mockAppPermissionKeys
- mockUseAppInfoActions.panelOpen = false
- mockUseAppInfoActions.activeModal = null
- })
-
- it('should return null when appDetail is not available', () => {
- mockUseAppInfoActions.appDetail = undefined as unknown as App & Partial
- const { container } = render()
- expect(container.innerHTML).toBe('')
- })
-
- it('should render trigger when not onlyShowDetail', () => {
- render()
- expect(screen.getByTestId('trigger'))!.toBeInTheDocument()
- })
-
- it('should not mount detail layer while the app info panel is closed', () => {
- render()
- expect(mockDetailPanel).not.toHaveBeenCalled()
- expect(mockModals).not.toHaveBeenCalled()
- })
-
- it('should not render trigger when onlyShowDetail is true', () => {
- render()
- expect(screen.queryByTestId('trigger')).not.toBeInTheDocument()
- })
-
- it('should pass expand prop to trigger', () => {
- render()
- expect(screen.getByTestId('trigger'))!.toHaveAttribute('data-expand', 'true')
-
- const { unmount } = render()
- const triggers = screen.getAllByTestId('trigger')
- expect(triggers[triggers.length - 1])!.toHaveAttribute('data-expand', 'false')
- unmount()
- })
-
- it('should toggle panel when trigger is clicked and user is editor', async () => {
- const user = userEvent.setup()
- render()
-
- await user.click(screen.getByTestId('trigger'))
-
- expect(mockSetPanelOpen).toHaveBeenCalled()
- const updater = mockSetPanelOpen.mock.calls[0]![0] as (v: boolean) => boolean
- expect(updater(false)).toBe(true)
- expect(updater(true)).toBe(false)
- })
-
- it('should not toggle panel when app ACL does not allow layout access', async () => {
- const user = userEvent.setup()
- mockAppPermissionKeys = []
- mockUseAppInfoActions.appDetail.permission_keys = mockAppPermissionKeys
- render()
-
- await user.click(screen.getByTestId('trigger'))
-
- expect(mockSetPanelOpen).not.toHaveBeenCalled()
- })
-
- it('should show detail panel based on panelOpen when not onlyShowDetail', () => {
- mockUseAppInfoActions.panelOpen = true
- render()
- expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument()
- expect(mockDetailPanel).toHaveBeenCalled()
- })
-
- it('should show detail panel based on openState when onlyShowDetail', () => {
- render()
- expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument()
- })
-
- it('should hide detail panel when openState is false and onlyShowDetail', () => {
- render()
- expect(screen.queryByTestId('detail-panel')).not.toBeInTheDocument()
- expect(mockDetailPanel).not.toHaveBeenCalled()
- expect(mockModals).not.toHaveBeenCalled()
- })
-})
diff --git a/web/app/components/app-sidebar/app-info/index.tsx b/web/app/components/app-sidebar/app-info/index.tsx
index f08c1f12ed8..ea1665d0dbd 100644
--- a/web/app/components/app-sidebar/app-info/index.tsx
+++ b/web/app/components/app-sidebar/app-info/index.tsx
@@ -5,7 +5,6 @@ import { getAppACLCapabilities } from '@/utils/permission'
import AppInfoDetailPanel from './app-info-detail-panel'
import AppInfoModals from './app-info-modals'
import AppInfoTrigger from './app-info-trigger'
-import { useAppInfoActions } from './use-app-info-actions'
type IAppInfoProps = {
expand: boolean
@@ -122,16 +121,3 @@ export const AppInfoView = ({
)
}
-
-const AppInfo = ({ onDetailExpand, ...props }: IAppInfoProps) => {
- const actions = useAppInfoActions({ onDetailExpand })
-
- return (
-
- )
-}
-
-export default React.memo(AppInfo)
diff --git a/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx
index 1add8601c40..e60576004b3 100644
--- a/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx
+++ b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'
-import VarHighlight, { varHighlightHTML } from '../index'
+import VarHighlight from '../index'
describe('VarHighlight', () => {
beforeEach(() => {
@@ -35,32 +35,4 @@ describe('VarHighlight', () => {
expect(container.firstChild).toHaveClass('mt-2')
})
})
-
- // Escaping HTML via helper
- describe('varHighlightHTML', () => {
- it('should escape dangerous characters before returning HTML string', () => {
- // Arrange
- const props = { name: '' }
-
- // Act
- const html = varHighlightHTML(props)
-
- // Assert
- expect(html).toContain('<script>alert('xss')</script>')
- expect(html).not.toContain('')).not.toContain('