From 94271b5d631348af8b3876eb39480170a90d4d05 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Tue, 21 Apr 2026 15:04:52 +0800 Subject: [PATCH] refactor(web): complete migration from PortalToFollowElem to Popover component in tests This commit finalizes the transition from the PortalToFollowElem to the Popover component across various test files. The updates include the implementation of Popover's context and trigger handling, ensuring consistent behavior in the UI. All relevant tests have been adjusted to reflect these changes, enhancing the overall test coverage and reliability. --- .../context-var/__tests__/index.spec.tsx | 84 ++++++++++++++++--- .../__tests__/selector-entry.spec.tsx | 57 ++++++++++++- .../__tests__/tool-picker.spec.tsx | 77 ++++++++++------- .../tools/labels/__tests__/selector.spec.tsx | 61 ++++++++++++++ .../__tests__/member-selector.spec.tsx | 55 ++++++++++++ 5 files changed, 292 insertions(+), 42 deletions(-) diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx index 6726ba0583..1754d01b8a 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx @@ -10,6 +10,72 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => '/test', })) +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactElement }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return React.cloneElement(render, { + 'data-testid': 'popover-trigger', + 'onClick': (e: React.MouseEvent) => { + render.props.onClick?.(e) + if (!e.defaultPrevented) + setOpen(!open) + }, + }) + } + + const PopoverContent = ({ + children, + ...props + }: React.HTMLAttributes & { children?: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + if (!open) + return null + + return ( +
+ {children} +
+ ) + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + type PortalToFollowElemProps = { children: React.ReactNode open?: boolean @@ -209,20 +275,17 @@ describe('ContextVar', () => { // Act render() - const triggers = screen.getAllByTestId('portal-trigger') - const varPickerTrigger = triggers[triggers.length - 1] + const varPickerTrigger = screen.getByTestId('popover-trigger') await user.click(varPickerTrigger!) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Select a different option - const options = screen.getAllByText('var2') - expect(options.length).toBeGreaterThan(0) - await user.click(options[0]!) + await user.click(screen.getByText('var2')) // Assert expect(onChange).toHaveBeenCalledWith('var2') - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) it('should toggle dropdown when clicking the trigger button', async () => { @@ -233,16 +296,15 @@ describe('ContextVar', () => { // Act render() - const triggers = screen.getAllByTestId('portal-trigger') - const varPickerTrigger = triggers[triggers.length - 1] + const varPickerTrigger = screen.getByTestId('popover-trigger') // Open dropdown await user.click(varPickerTrigger!) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Close dropdown await user.click(varPickerTrigger!) - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx index 37d828591f..a441720392 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx @@ -4,6 +4,61 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { SubscriptionSelectorEntry } from '../selector-entry' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactElement }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return React.cloneElement(render, { + onClick: (e: React.MouseEvent) => { + render.props.onClick?.(e) + if (!e.defaultPrevented) + setOpen(!open) + }, + }) + } + + const PopoverContent = ({ children }: { children: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + let mockSubscriptions: TriggerSubscription[] = [] const mockRefetch = vi.fn() @@ -92,6 +147,6 @@ describe('SubscriptionSelectorEntry', () => { fireEvent.click(screen.getByRole('button', { name: 'Subscription One' })) expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-1', name: 'Subscription One' }), expect.any(Function)) - expect(screen.queryByText('Subscription One')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx index e89b1b3161..a83ec53be3 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx @@ -4,8 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginSource } from '@/app/components/plugins/types' import ToolPicker from '../tool-picker' -let portalOpen = false - const mockInstalledPluginList = vi.hoisted(() => ({ data: { plugins: [] as PluginDetail[], @@ -21,33 +19,53 @@ vi.mock('@/app/components/base/loading', () => ({ default: () =>
loading
, })) -vi.mock('@/app/components/base/portal-to-follow-elem', async () => { - const _React = await import('react') +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => ( + onOpenChange?.(nextOpen) }}> + {children} + + ) + + const PopoverTrigger = ({ render }: { render: React.ReactElement }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return React.cloneElement(render, { + onClick: (e: React.MouseEvent) => { + render.props.onClick?.(e) + if (!e.defaultPrevented) + setOpen(!open) + }, + }) + } + + const PopoverContent = ({ + children, + className, + }: { + children: React.ReactNode + className?: string + }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + return { - PortalToFollowElem: ({ - open, - children, - }: { - open: boolean - children: React.ReactNode - }) => { - portalOpen = open - return
{children}
- }, - PortalToFollowElemTrigger: ({ - children, - onClick, - }: { - children: React.ReactNode - onClick: () => void - }) => , - PortalToFollowElemContent: ({ - children, - className, - }: { - children: React.ReactNode - className?: string - }) => portalOpen ?
{children}
: null, + Popover, + PopoverTrigger, + PopoverContent, } }) @@ -118,7 +136,6 @@ const createPlugin = ( describe('ToolPicker', () => { beforeEach(() => { vi.clearAllMocks() - portalOpen = false mockInstalledPluginList.data = { plugins: [], } @@ -137,7 +154,7 @@ describe('ToolPicker', () => { />, ) - fireEvent.click(screen.getByTestId('trigger')) + fireEvent.click(screen.getByText('trigger')) expect(onShowChange).toHaveBeenCalledWith(true) }) diff --git a/web/app/components/tools/labels/__tests__/selector.spec.tsx b/web/app/components/tools/labels/__tests__/selector.spec.tsx index b495d2d227..c6f2d828fb 100644 --- a/web/app/components/tools/labels/__tests__/selector.spec.tsx +++ b/web/app/components/tools/labels/__tests__/selector.spec.tsx @@ -2,6 +2,67 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import LabelSelector from '../selector' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactElement }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return React.cloneElement(render, { + onClick: (e: React.MouseEvent) => { + render.props.onClick?.(e) + if (!e.defaultPrevented) + setOpen(!open) + }, + }) + } + + const PopoverContent = ({ + children, + ...props + }: React.HTMLAttributes & { children?: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + if (!open) + return null + + return
{children}
+ } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + // Mock useTags hook with controlled test data const mockTags = [ { name: 'agent', label: 'Agent' }, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx index 823aa68047..058b78254e 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx @@ -2,6 +2,61 @@ import type { Member } from '@/models/common' import { fireEvent, render, screen } from '@testing-library/react' import MemberSelector from '../member-selector' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: import('react').ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: import('react').ReactElement }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return React.cloneElement(render, { + onClick: (e: import('react').MouseEvent) => { + render.props.onClick?.(e) + if (!e.defaultPrevented) + setOpen(!open) + }, + }) + } + + const PopoverContent = ({ children }: { children: import('react').ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + const mockMemberList = vi.hoisted(() => vi.fn()) vi.mock('../member-list', () => ({