From 5de4868f4f0b46eddcb312ff45fe7953a5d31b96 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Mon, 20 Apr 2026 18:58:54 +0800 Subject: [PATCH] refactor(web): update type definitions and improve test mocks for Popover components --- .../date-picker/__tests__/index.spec.tsx | 7 ++- .../time-picker/__tests__/index.spec.tsx | 7 ++- .../base/date-and-time-picker/types.ts | 4 +- .../__tests__/setting-modal.spec.tsx | 9 +--- .../plugins/context-block/component.tsx | 21 ++++---- .../plugins/history-block/component.tsx | 21 ++++---- .../steps/__tests__/preview-panel.spec.tsx | 19 +++---- .../search-box/__tests__/index.spec.tsx | 25 +++++---- .../__tests__/index.spec.tsx | 54 ++++++++++++++----- .../__tests__/index.spec.tsx | 38 +++---------- .../publisher/__tests__/popup.spec.tsx | 16 +++--- .../rag-pipeline-header/publisher/index.tsx | 2 +- .../toolbar/__tests__/index.spec.tsx | 9 ++-- .../note-editor/toolbar/color-picker.tsx | 2 +- .../toolbar/font-size-selector.tsx | 2 +- 15 files changed, 131 insertions(+), 105 deletions(-) diff --git a/web/app/components/base/date-and-time-picker/date-picker/__tests__/index.spec.tsx b/web/app/components/base/date-and-time-picker/date-picker/__tests__/index.spec.tsx index 0e72a69351..d37647f358 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/date-and-time-picker/date-picker/__tests__/index.spec.tsx @@ -5,7 +5,12 @@ import DatePicker from '../index' vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover')) vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ children, onClick, disabled, className }: Record) => ( + Button: ({ children, onClick, disabled, className }: { + children?: React.ReactNode + onClick?: () => void + disabled?: boolean + className?: string + }) => ( diff --git a/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx b/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx index 0857bb35f8..0d02b3b5d5 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx @@ -5,7 +5,12 @@ import TimePicker from '../index' vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover')) vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ children, onClick, disabled, className }: Record) => ( + Button: ({ children, onClick, disabled, className }: { + children?: React.ReactNode + onClick?: () => void + disabled?: boolean + className?: string + }) => ( diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 0068ec22ac..2773fb7bc7 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -28,7 +28,7 @@ export type DatePickerProps = { onChange: (date: Dayjs | undefined) => void onClear: () => void triggerWrapClassName?: string - renderTrigger?: (props: TriggerProps) => React.ReactNode + renderTrigger?: (props: TriggerProps) => React.ReactElement minuteFilter?: (minutes: string[]) => string[] popupZIndexClassname?: string noConfirm?: boolean @@ -62,7 +62,7 @@ export type TimePickerProps = { placeholder?: string onChange: (date: Dayjs | undefined) => void onClear: () => void - renderTrigger?: (props: TriggerParams) => React.ReactNode + renderTrigger?: (props: TriggerParams) => React.ReactElement title?: string minuteFilter?: (minutes: string[]) => string[] popupClassName?: string diff --git a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx index dc111a680b..6259c7cb4f 100644 --- a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx @@ -61,7 +61,7 @@ describe('FileUploadSettings (setting-modal)', () => { }) }) - it('should call onOpen with toggle function when trigger is clicked', () => { + it('should call onOpen with true when trigger is clicked', () => { const onOpen = vi.fn() renderWithProvider( @@ -71,12 +71,7 @@ describe('FileUploadSettings (setting-modal)', () => { fireEvent.click(screen.getByText('Upload Settings')) - expect(onOpen).toHaveBeenCalled() - // The toggle function should flip the open state - const toggleFn = onOpen.mock.calls[0]![0] - expect(typeof toggleFn).toBe('function') - expect(toggleFn(false)).toBe(true) - expect(toggleFn(true)).toBe(false) + expect(onOpen).toHaveBeenCalledWith(true) }) it('should not call onOpen when disabled', () => { diff --git a/web/app/components/base/prompt-editor/plugins/context-block/component.tsx b/web/app/components/base/prompt-editor/plugins/context-block/component.tsx index b430692b94..05fff5b4e8 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/component.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react' import type { Dataset } from './index' +import type { EventEmitterValue } from '@/context/event-emitter' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { useState } from 'react' @@ -16,11 +17,6 @@ type ContextBlockComponentProps = { canNotAddContext?: boolean } -type DatasetsEventPayload = { - type?: string - payload?: Dataset[] -} - const ContextBlockComponent: FC = ({ nodeKey, datasets = [], @@ -33,9 +29,12 @@ const ContextBlockComponent: FC = ({ const { eventEmitter } = useEventEmitterContextContext() const [localDatasets, setLocalDatasets] = useState(datasets) - eventEmitter?.useSubscription((event?: DatasetsEventPayload) => { - if (event?.type === UPDATE_DATASETS_EVENT_EMITTER && event.payload) - setLocalDatasets(event.payload) + eventEmitter?.useSubscription((event?: EventEmitterValue) => { + if (typeof event === 'string') + return + + if (event?.type === UPDATE_DATASETS_EVENT_EMITTER && Array.isArray(event.payload)) + setLocalDatasets(event.payload as Dataset[]) }) return ( @@ -56,12 +55,14 @@ const ContextBlockComponent: FC = ({ > e.preventDefault()} > {localDatasets.length} diff --git a/web/app/components/base/prompt-editor/plugins/history-block/component.tsx b/web/app/components/base/prompt-editor/plugins/history-block/component.tsx index 827becf857..a9bc68ac30 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/component.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react' import type { RoleName } from './index' +import type { EventEmitterValue } from '@/context/event-emitter' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiMoreFill, @@ -18,11 +19,6 @@ type HistoryBlockComponentProps = { onEditRole: () => void } -type HistoryEventPayload = { - type?: string - payload?: RoleName -} - const HistoryBlockComponent: FC = ({ nodeKey, roleName = { user: '', assistant: '' }, @@ -34,9 +30,12 @@ const HistoryBlockComponent: FC = ({ const { eventEmitter } = useEventEmitterContextContext() const [localRoleName, setLocalRoleName] = useState(roleName) - eventEmitter?.useSubscription((event?: HistoryEventPayload) => { - if (event?.type === UPDATE_HISTORY_EVENT_EMITTER && event.payload) - setLocalRoleName(event.payload) + eventEmitter?.useSubscription((event?: EventEmitterValue) => { + if (typeof event === 'string') + return + + if (event?.type === UPDATE_HISTORY_EVENT_EMITTER && event.payload && typeof event.payload === 'object') + setLocalRoleName(event.payload as RoleName) }) return ( @@ -56,12 +55,14 @@ const HistoryBlockComponent: FC = ({ > e.preventDefault()} > diff --git a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx index 11f1286306..9c29206e7d 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx @@ -231,8 +231,9 @@ describe('StepTwoPreview', () => { describe('Props Passing', () => { it('should render preview button when isIdle is true', () => { render() - // ChunkPreview shows a preview button when idle - const previewButton = screen.queryByRole('button') + const previewButton = screen.getByRole('button', { + name: 'datasetPipeline.addDocuments.stepTwo.previewChunks', + }) expect(previewButton).toBeInTheDocument() }) @@ -240,13 +241,13 @@ describe('StepTwoPreview', () => { const onPreview = vi.fn() render() - // Find and click the preview button - const buttons = screen.getAllByRole('button') - const previewButton = buttons.find(btn => btn.textContent?.toLowerCase().includes('preview')) - if (previewButton) { - previewButton.click() - expect(onPreview).toHaveBeenCalled() - } + const previewButton = screen.getByRole('button', { + name: 'datasetPipeline.addDocuments.stepTwo.previewChunks', + }) + + previewButton.click() + + expect(onPreview).toHaveBeenCalled() }) }) diff --git a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx index 8609ba5539..51b4087659 100644 --- a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx @@ -62,31 +62,38 @@ vi.mock('@/app/components/plugins/hooks', () => ({ }), })) -// Mock portal-to-follow-elem with shared open state +// Mock popover with shared open state let mockPortalOpenState = false +let mockPopoverOnOpenChange: ((open: boolean) => void) | undefined -vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ - PortalToFollowElem: ({ children, open }: { +vi.mock('@langgenius/dify-ui/popover', () => ({ + Popover: ({ children, open, onOpenChange }: { children: React.ReactNode open: boolean + onOpenChange?: (open: boolean) => void }) => { mockPortalOpenState = open + mockPopoverOnOpenChange = onOpenChange return (
{children}
) }, - PortalToFollowElemTrigger: ({ children, onClick, className }: { - children: React.ReactNode - onClick: () => void + PopoverTrigger: ({ children, render, className }: { + children?: React.ReactNode + render?: React.ReactNode className?: string }) => ( -
- {children} +
mockPopoverOnOpenChange?.(!mockPortalOpenState)} + className={className} + > + {render ?? children}
), - PortalToFollowElemContent: ({ children, className }: { + PopoverContent: ({ children, className }: { children: React.ReactNode className?: string }) => { diff --git a/web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx index f30b5fb5fa..46493a87df 100644 --- a/web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ import type { Category, Tag } from '../constant' import type { FilterState } from '../index' import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react' +import { createContext, useContext } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' // ==================== Imports (after mocks) ==================== @@ -68,19 +69,47 @@ vi.mock('../../../hooks', () => ({ }), })) -// Track portal open state for testing -let mockPortalOpenState = false +type MockPopoverContextValue = { + open: boolean + onOpenChange?: (open: boolean) => void +} -vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ - PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => { - mockPortalOpenState = open - return
{children}
- }, - PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => ( -
{children}
+const MockPopoverContext = createContext({ + open: false, +}) + +vi.mock('@langgenius/dify-ui/popover', () => ({ + Popover: ({ children, open, onOpenChange }: { + children: React.ReactNode + open: boolean + onOpenChange?: (open: boolean) => void + }) => ( + +
{children}
+
), - PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => { - if (!mockPortalOpenState) + PopoverTrigger: ({ children, render, className }: { + children?: React.ReactNode + render?: React.ReactNode + className?: string + }) => { + const { open, onOpenChange } = useContext(MockPopoverContext) + return ( +
onOpenChange?.(!open)} + className={className} + > + {render ?? children} +
+ ) + }, + PopoverContent: ({ children, className }: { + children: React.ReactNode + className?: string + }) => { + const { open } = useContext(MockPopoverContext) + if (!open) return null return
{children}
}, @@ -457,7 +486,6 @@ describe('SearchBox Component', () => { describe('CategoriesFilter Component', () => { beforeEach(() => { vi.clearAllMocks() - mockPortalOpenState = false }) describe('Rendering', () => { @@ -694,7 +722,6 @@ describe('CategoriesFilter Component', () => { describe('TagFilter Component', () => { beforeEach(() => { vi.clearAllMocks() - mockPortalOpenState = false }) describe('Rendering', () => { @@ -857,7 +884,6 @@ describe('FilterManagement Component', () => { beforeEach(() => { vi.clearAllMocks() mockInitFilters = createFilterState() - mockPortalOpenState = false }) describe('Rendering', () => { diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx index dda765b48f..cff2a5f4c2 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx @@ -192,27 +192,6 @@ vi.mock('ahooks', () => ({ useKeyPress: vi.fn(), })) -let portalOpenState = false -vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ - PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: PropsWithChildren<{ - open: boolean - onOpenChange: (open: boolean) => void - placement?: string - offset?: unknown - }>) => { - portalOpenState = open - return
{children}
- }, - PortalToFollowElemTrigger: ({ children, onClick }: PropsWithChildren<{ onClick?: () => void }>) => ( -
{children}
- ), - PortalToFollowElemContent: ({ children }: PropsWithChildren) => { - if (!portalOpenState) - return null - return
{children}
- }, -})) - vi.mock('../../../publish-as-knowledge-pipeline-modal', () => ({ default: ({ onConfirm, onCancel }: { onConfirm: (name: string, icon: unknown, description?: string) => void @@ -229,7 +208,6 @@ vi.mock('../../../publish-as-knowledge-pipeline-modal', () => ({ describe('RagPipelineHeader', () => { beforeEach(() => { vi.clearAllMocks() - portalOpenState = false mockStoreState = { pipelineId: 'test-pipeline-id', showDebugAndPreviewPanel: false, @@ -351,7 +329,6 @@ describe('InputFieldButton', () => { describe('Publisher', () => { beforeEach(() => { vi.clearAllMocks() - portalOpenState = false }) describe('Rendering', () => { @@ -367,9 +344,9 @@ describe('Publisher', () => { expect(button)!.toHaveClass('px-2') }) - it('should render portal trigger element', () => { + it('should render publish trigger button', () => { render() - expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: /workflow\.common\.publish/i }))!.toBeInTheDocument() }) }) @@ -377,7 +354,7 @@ describe('Publisher', () => { it('should call handleSyncWorkflowDraft when opening', () => { render() - fireEvent.click(screen.getByTestId('portal-trigger')) + fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i })) expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true) }) @@ -385,12 +362,14 @@ describe('Publisher', () => { it('should toggle open state when trigger clicked', () => { render() - const portal = screen.getByTestId('portal-elem') - expect(portal)!.toHaveAttribute('data-open', 'false') + const trigger = screen.getByRole('button', { name: /workflow\.common\.publish/i }) + expect(trigger)!.toHaveAttribute('aria-expanded', 'false') - fireEvent.click(screen.getByTestId('portal-trigger')) + fireEvent.click(trigger) expect(mockHandleSyncWorkflowDraft).toHaveBeenCalled() + expect(trigger)!.toHaveAttribute('aria-expanded', 'true') + expect(screen.getByText(/workflow\.common\.publishUpdate/i))!.toBeInTheDocument() }) }) }) @@ -978,7 +957,6 @@ describe('RunMode', () => { describe('Integration', () => { beforeEach(() => { vi.clearAllMocks() - portalOpenState = false mockStoreState = { pipelineId: 'test-pipeline-id', showDebugAndPreviewPanel: false, diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 6cd00d93bc..dab8046c43 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -16,12 +16,16 @@ vi.mock('@langgenius/dify-ui/alert-dialog', () => ({ ) : null ), - AlertDialogActions: ({ children }: { children: unknown }) =>
{children}
, - AlertDialogCancelButton: ({ children }: { children: unknown }) => , - AlertDialogConfirmButton: ({ children, onClick, disabled }: Record) => , - AlertDialogContent: ({ children }: { children: unknown }) =>
{children}
, - AlertDialogDescription: ({ children }: { children: unknown }) =>
{children}
, - AlertDialogTitle: ({ children }: { children: unknown }) =>
{children}
, + AlertDialogActions: ({ children }: { children?: React.ReactNode }) =>
{children}
, + AlertDialogCancelButton: ({ children }: { children?: React.ReactNode }) => , + AlertDialogConfirmButton: ({ children, onClick, disabled }: { + children?: React.ReactNode + onClick?: () => void + disabled?: boolean + }) => , + AlertDialogContent: ({ children }: { children?: React.ReactNode }) =>
{children}
, + AlertDialogDescription: ({ children }: { children?: React.ReactNode }) =>
{children}
, + AlertDialogTitle: ({ children }: { children?: React.ReactNode }) =>
{children}
, })) const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx index d846d7d749..649b06ebca 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx @@ -27,7 +27,7 @@ const Publisher = () => { onOpenChange={handleOpenChange} > { expect(screen.getByText('workflow.nodes.note.editor.medium')).toBeInTheDocument() - const triggers = container.querySelectorAll('[data-state="closed"]') + const buttons = container.querySelectorAll('button[type="button"]') + fireEvent.click(buttons[0] as HTMLElement) - fireEvent.click(triggers[0] as HTMLElement) + await waitFor(() => { + expect(document.body.querySelectorAll('.group.relative').length).toBeGreaterThan(0) + }) - const colorOptions = document.body.querySelectorAll('[role="tooltip"] .group.relative') + const colorOptions = document.body.querySelectorAll('.group.relative') fireEvent.click(colorOptions[colorOptions.length - 1] as Element) diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx index 5669c92f50..2b12a947ea 100644 --- a/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx +++ b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx @@ -60,7 +60,7 @@ const ColorPicker = ({ onOpenChange={setOpen} > { onOpenChange={handleOpenFontSizeSelector} >