Merge commit 'fb41b215' into sandboxed-agent-rebase

Made-with: Cursor

# Conflicts:
#	.devcontainer/post_create_command.sh
#	api/commands.py
#	api/core/agent/cot_agent_runner.py
#	api/core/agent/fc_agent_runner.py
#	api/core/app/apps/workflow_app_runner.py
#	api/core/app/entities/queue_entities.py
#	api/core/app/entities/task_entities.py
#	api/core/workflow/workflow_entry.py
#	api/dify_graph/enums.py
#	api/dify_graph/graph/graph.py
#	api/dify_graph/graph_events/node.py
#	api/dify_graph/model_runtime/entities/message_entities.py
#	api/dify_graph/node_events/node.py
#	api/dify_graph/nodes/agent/agent_node.py
#	api/dify_graph/nodes/base/__init__.py
#	api/dify_graph/nodes/base/entities.py
#	api/dify_graph/nodes/base/node.py
#	api/dify_graph/nodes/llm/entities.py
#	api/dify_graph/nodes/llm/node.py
#	api/dify_graph/nodes/tool/tool_node.py
#	api/pyproject.toml
#	api/uv.lock
#	web/app/components/base/avatar/__tests__/index.spec.tsx
#	web/app/components/base/avatar/index.tsx
#	web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx
#	web/app/components/base/file-uploader/file-from-link-or-local/index.tsx
#	web/app/components/base/prompt-editor/index.tsx
#	web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx
#	web/app/components/header/account-dropdown/index.spec.tsx
#	web/app/components/share/text-generation/index.tsx
#	web/app/components/workflow/block-selector/tool/action-item.tsx
#	web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx
#	web/app/components/workflow/hooks/use-edges-interactions.ts
#	web/app/components/workflow/hooks/use-nodes-interactions.ts
#	web/app/components/workflow/index.tsx
#	web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
#	web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx
#	web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx
#	web/app/components/workflow/nodes/loop/use-interactions.ts
#	web/contract/router.ts
#	web/env.ts
#	web/eslint-suppressions.json
#	web/package.json
#	web/pnpm-lock.yaml
This commit is contained in:
Novice
2026-03-23 10:52:06 +08:00
1395 changed files with 167201 additions and 73658 deletions

View File

@ -3,9 +3,8 @@ import type { ImgHTMLAttributes } from 'react'
import type { EmbeddedChatbotContextValue } from '../../context'
import type { AppData } from '@/models/share'
import type { SystemFeatures } from '@/types/feature'
import { render, screen, waitFor } from '@testing-library/react'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { InstallationScope, LicenseStatus } from '@/types/feature'
import { useEmbeddedChatbotContext } from '../../context'
@ -122,6 +121,18 @@ describe('EmbeddedChatbot Header', () => {
Object.defineProperty(window, 'top', { value: window, configurable: true })
})
const dispatchChatbotConfigMessage = async (origin: string, payload: { isToggledByButton: boolean, isDraggable: boolean }) => {
await act(async () => {
window.dispatchEvent(new MessageEvent('message', {
origin,
data: {
type: 'dify-chatbot-config',
payload,
},
}))
})
}
describe('Desktop Rendering', () => {
it('should render desktop header with branding by default', async () => {
render(<Header title="Test Chatbot" />)
@ -166,7 +177,23 @@ describe('EmbeddedChatbot Header', () => {
expect(img).toHaveAttribute('src', 'https://example.com/workspace.png')
})
it('should render Dify logo by default when no branding or custom logo is provided', () => {
it('should render Dify logo by default when branding enabled is true but no logo provided', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
systemFeatures: {
...defaultSystemFeatures,
branding: {
...defaultSystemFeatures.branding,
enabled: true,
workspace_logo: '',
},
},
setSystemFeatures: vi.fn(),
}))
render(<Header title="Test Chatbot" />)
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
})
it('should render Dify logo when branding is disabled', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
systemFeatures: {
...defaultSystemFeatures,
@ -198,6 +225,20 @@ describe('EmbeddedChatbot Header', () => {
expect(screen.queryByTestId('webapp-brand')).not.toBeInTheDocument()
})
it('should render divider only when currentConversationId is present', () => {
vi.mocked(useEmbeddedChatbotContext).mockReturnValue({ ...defaultContext } as EmbeddedChatbotContextValue)
const { unmount } = render(<Header title="Test Chatbot" />)
expect(screen.getByTestId('divider')).toBeInTheDocument()
unmount()
vi.mocked(useEmbeddedChatbotContext).mockReturnValue({
...defaultContext,
currentConversationId: '',
} as EmbeddedChatbotContextValue)
render(<Header title="Test Chatbot" />)
expect(screen.queryByTestId('divider')).not.toBeInTheDocument()
})
it('should render reset button when allowResetChat is true and conversation exists', () => {
render(<Header title="Test Chatbot" allowResetChat={true} />)
@ -268,6 +309,42 @@ describe('EmbeddedChatbot Header', () => {
expect(screen.getByTestId('mobile-reset-chat-button')).toBeInTheDocument()
})
it('should NOT render mobile reset button when currentConversationId is missing', () => {
vi.mocked(useEmbeddedChatbotContext).mockReturnValue({
...defaultContext,
currentConversationId: '',
} as EmbeddedChatbotContextValue)
render(<Header title="Mobile Chatbot" isMobile allowResetChat />)
expect(screen.queryByTestId('mobile-reset-chat-button')).not.toBeInTheDocument()
})
it('should render ViewFormDropdown in mobile when conditions are met', () => {
vi.mocked(useEmbeddedChatbotContext).mockReturnValue({
...defaultContext,
inputsForms: [{ id: '1' }],
} as EmbeddedChatbotContextValue)
render(<Header title="Mobile Chatbot" isMobile />)
expect(screen.getByTestId('view-form-dropdown')).toBeInTheDocument()
})
it('should handle mobile expand button', async () => {
const user = userEvent.setup()
const mockPostMessage = setupIframe()
render(<Header title="Mobile Chatbot" isMobile />)
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: false })
const expandBtn = await screen.findByTestId('mobile-expand-button')
expect(expandBtn).toBeInTheDocument()
await user.click(expandBtn)
expect(mockPostMessage).toHaveBeenCalledWith(
{ type: 'dify-chatbot-expand-change' },
'https://parent.com',
)
})
})
describe('Iframe Communication', () => {
@ -286,13 +363,7 @@ describe('EmbeddedChatbot Header', () => {
const mockPostMessage = setupIframe()
render(<Header title="Iframe" />)
window.dispatchEvent(new MessageEvent('message', {
origin: 'https://parent.com',
data: {
type: 'dify-chatbot-config',
payload: { isToggledByButton: true, isDraggable: false },
},
}))
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: false })
const expandBtn = await screen.findByTestId('expand-button')
expect(expandBtn).toBeInTheDocument()
@ -310,13 +381,7 @@ describe('EmbeddedChatbot Header', () => {
setupIframe()
render(<Header title="Iframe" />)
window.dispatchEvent(new MessageEvent('message', {
origin: 'https://parent.com',
data: {
type: 'dify-chatbot-config',
payload: { isToggledByButton: true, isDraggable: true },
},
}))
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: true })
await waitFor(() => {
expect(screen.queryByTestId('expand-button')).not.toBeInTheDocument()
@ -327,20 +392,43 @@ describe('EmbeddedChatbot Header', () => {
setupIframe()
render(<Header title="Iframe" />)
window.dispatchEvent(new MessageEvent('message', {
origin: 'https://secure.com',
data: { type: 'dify-chatbot-config', payload: { isToggledByButton: true, isDraggable: false } },
}))
await dispatchChatbotConfigMessage('https://secure.com', { isToggledByButton: true, isDraggable: false })
await screen.findByTestId('expand-button')
window.dispatchEvent(new MessageEvent('message', {
origin: 'https://malicious.com',
data: { type: 'dify-chatbot-config', payload: { isToggledByButton: false, isDraggable: false } },
}))
await dispatchChatbotConfigMessage('https://malicious.com', { isToggledByButton: false, isDraggable: false })
// Should still be visible (not hidden by the malicious message)
expect(screen.getByTestId('expand-button')).toBeInTheDocument()
})
it('should ignore non-config messages for origin locking', async () => {
setupIframe()
render(<Header title="Iframe" />)
await act(async () => {
window.dispatchEvent(new MessageEvent('message', {
origin: 'https://first.com',
data: { type: 'other-type' },
}))
})
await dispatchChatbotConfigMessage('https://second.com', { isToggledByButton: true, isDraggable: false })
// Should lock to second.com
const expandBtn = await screen.findByTestId('expand-button')
expect(expandBtn).toBeInTheDocument()
})
it('should NOT handle toggle expand if showToggleExpandButton is false', async () => {
const mockPostMessage = setupIframe()
render(<Header title="Iframe" />)
// Directly call handleToggleExpand would require more setup, but we can verify it doesn't trigger unexpectedly
expect(mockPostMessage).not.toHaveBeenCalledWith(
expect.objectContaining({ type: 'dify-chatbot-expand-change' }),
expect.anything(),
)
})
})
describe('Edge Cases', () => {