('i18next')
+ const { createReactI18nextMock } = await import('@/test/i18n-mock')
+ const { default: translations } = await import('@/i18n/locales/en-US/workflow-human-input.json')
+ const i18n = createInstance()
+ await i18n.init({
+ lng: 'en-US',
+ fallbackLng: false,
+ keySeparator: false,
+ resources: { 'en-US': { workflowHumanInput: translations } },
})
-
-vi.mock('../email-configure-modal', () => ({
- default: (props: EmailConfigureModalProps) => {
- return props.open ? (
-
-
-
-
- ) : null
- },
-}))
-
-vi.mock('../test-email-sender', () => ({
- default: (props: TestEmailSenderProps) => {
- return props.open ? (
-
-
-
-
- ) : null
- },
-}))
+ return {
+ ...actual,
+ ...createReactI18nextMock(),
+ Trans: (props: ComponentProps) => ,
+ }
+})
const createEmailConfig = (overrides: Partial = {}): EmailConfig => ({
recipients: {
@@ -96,177 +68,201 @@ const formInputs: FormInputItem[] = [
},
]
-const availableNodes = [
- {
- id: 'start',
- data: {
- title: 'Start',
- type: 'start',
- },
- },
-] as unknown as Node[]
-
-const nodesOutputVars = [
+const nodesOutputVars: NodeOutPutVar[] = [
{
nodeId: 'start',
title: 'Start',
- vars: [],
+ vars: [{ variable: 'name', type: VarType.string }],
},
-] as NodeOutPutVar[]
+]
+
+function DeliveryMethodHarness({
+ availableNodes,
+ initialMethod,
+ readonly = false,
+}: {
+ availableNodes: Node[]
+ initialMethod: DeliveryMethod
+ readonly?: boolean
+}) {
+ const [method, setMethod] = useState(initialMethod)
+ return (
+ <>
+ {method && (
+ setMethod((current) => (current?.type === type ? null : current))}
+ readonly={readonly}
+ />
+ )}
+
+ >
+ )
+}
+
+const renderMethod = (initialMethod: DeliveryMethod, readonly = false) => {
+ const start = createStartNode({
+ id: 'start',
+ data: {
+ variables: [
+ { variable: 'name', label: 'Name', type: InputVarType.textInput, required: false },
+ ],
+ },
+ })
+ const humanInputData: HumanInputNodeType = {
+ title: 'Human input',
+ desc: '',
+ type: BlockEnum.HumanInput,
+ delivery_methods: [initialMethod],
+ form_content: 'Hello {{#url#}}',
+ inputs: formInputs,
+ user_actions: [],
+ timeout: 3,
+ timeout_unit: 'day',
+ }
+ const humanInput = createNode({ id: 'human-input', data: humanInputData })
+ return renderWorkflowEditor(
+ ,
+ {
+ nodes: [start, humanInput],
+ edges: [
+ createEdge({
+ source: 'start',
+ target: 'human-input',
+ data: { sourceType: BlockEnum.Start, targetType: BlockEnum.HumanInput },
+ }),
+ ],
+ },
+ )
+}
+
+const savedMethod = () => screen.getByRole('status', { name: 'Saved delivery method' })
describe('human-input/delivery-method/method-item', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('should toggle and delete a webapp delivery method', async () => {
+ it('toggles and deletes a webapp delivery method through controlled owner state', async () => {
const user = userEvent.setup()
- const handleChange = vi.fn()
- const handleDelete = vi.fn()
+ renderMethod({ id: 'webapp-1', type: DeliveryMethodType.WebApp, enabled: true })
- render(
- ,
+ const enabled = screen.getByRole('switch')
+ expect(enabled).toBeChecked()
+ await user.click(enabled)
+ expect(enabled).not.toBeChecked()
+ expect(savedMethod()).toHaveTextContent(
+ JSON.stringify({ id: 'webapp-1', type: DeliveryMethodType.WebApp, enabled: false }),
)
- await user.click(screen.getByRole('switch'))
- expect(handleChange).toHaveBeenCalledWith({
- id: 'webapp-1',
- type: DeliveryMethodType.WebApp,
- enabled: false,
+ await user.click(screen.getByRole('button', { name: 'common.operation.remove' }))
+ expect(savedMethod()).toHaveTextContent('removed')
+ expect(screen.queryByRole('switch')).not.toBeInTheDocument()
+ })
+
+ it('uses the real test sender and saves subject and body edits from the real configuration form', async () => {
+ const user = userEvent.setup()
+ // The jump button exists only for non-debug email with specific recipients; a debug fixture
+ // cannot reach this production branch.
+ renderMethod({
+ id: 'email-1',
+ type: DeliveryMethodType.Email,
+ enabled: true,
+ config: createEmailConfig(),
})
- const deleteButton = screen.getByRole('button', { name: 'common.operation.remove' })
-
- await user.click(deleteButton)
- expect(handleDelete).toHaveBeenCalledWith(DeliveryMethodType.WebApp)
- })
-
- it('should open configure and test flows for an email method with config', async () => {
- const user = userEvent.setup()
- const handleChange = vi.fn()
- const handleDelete = vi.fn()
-
- render(
- ,
- )
-
- expect(screen.getByText('DEBUG'))!.toBeInTheDocument()
-
- const testButton = screen.getByRole('button', { name: /emailSender.testSendTipInDebugMode/ })
+ const testButton = screen.getByRole('button', { name: /emailSender.testSendTip/ })
+ await user.click(testButton)
+ let sender = screen.getByRole('dialog', { name: /emailSender.title/ })
+ await user.click(within(sender).getByRole('button', { name: 'common.operation.close' }))
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
await user.click(testButton)
- expect(screen.getByTestId('test-email-sender'))!.toBeInTheDocument()
-
- await user.click(screen.getByRole('button', { name: 'close-test-sender' }))
- expect(screen.queryByTestId('test-email-sender')).not.toBeInTheDocument()
-
- await user.click(testButton)
- await user.click(screen.getByRole('button', { name: 'jump-to-config' }))
- expect(screen.queryByTestId('test-email-sender')).not.toBeInTheDocument()
- expect(screen.getByTestId('email-configure-modal'))!.toBeInTheDocument()
-
- await user.click(screen.getByRole('button', { name: 'close-email-config' }))
- expect(screen.queryByTestId('email-configure-modal')).not.toBeInTheDocument()
+ sender = screen.getByRole('dialog', { name: /emailSender.title/ })
+ await user.click(within(sender).getByRole('button', { name: 'enable Debug Mode' }))
+ const configure = await screen.findByRole('dialog', { name: /emailConfigure.title/ })
+ expect(screen.queryByRole('dialog', { name: /emailSender.title/ })).not.toBeInTheDocument()
+ await user.click(within(configure).getByRole('button', { name: 'common.operation.cancel' }))
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
await user.click(screen.getByRole('button', { name: 'workflow.common.configure' }))
- expect(screen.getByTestId('email-configure-modal'))!.toBeInTheDocument()
+ const subject = screen.getByRole('textbox', { name: /emailConfigure.subject$/ })
+ await user.clear(subject)
+ await user.type(subject, 'Configured subject')
+ const body = screen.getByRole('textbox', { name: /emailConfigure.body$/ })
+ await replacePromptEditorText(body, 'Configured body {{#url#}}')
+ await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
- await user.click(screen.getByRole('button', { name: 'confirm-email-config' }))
- expect(handleChange).toHaveBeenCalledWith(
- expect.objectContaining({
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
+ expect(savedMethod()).toHaveTextContent(
+ JSON.stringify({
id: 'email-1',
type: DeliveryMethodType.Email,
- config: expect.objectContaining({
+ enabled: true,
+ config: createEmailConfig({
subject: 'Configured subject',
+ body: 'Configured body {{#url#}}',
}),
}),
)
await user.click(screen.getByRole('button', { name: 'common.operation.remove' }))
- expect(handleDelete).toHaveBeenCalledWith(DeliveryMethodType.Email)
+ expect(savedMethod()).toHaveTextContent('removed')
})
- it('should open email config from the unconfigured button and from the test sender jump action', async () => {
+ it('opens and dismisses the real configuration form for an unconfigured email method', async () => {
const user = userEvent.setup()
- render(
- ,
- )
+ const method: DeliveryMethod = {
+ id: 'email-2',
+ type: DeliveryMethodType.Email,
+ enabled: false,
+ config: undefined,
+ }
+ renderMethod(method)
await user.click(
screen.getByRole('button', {
name: /workflowHumanInput.nodes.humanInput.deliveryMethod.notConfigured/i,
}),
)
- expect(screen.getByTestId('email-configure-modal'))!.toBeInTheDocument()
-
- await user.click(screen.getByRole('button', { name: 'close-email-config' }))
- expect(screen.queryByTestId('email-configure-modal')).not.toBeInTheDocument()
+ const configure = screen.getByRole('dialog', { name: /emailConfigure.title/ })
+ expect(within(configure).getByRole('textbox', { name: /emailConfigure.subject$/ })).toHaveValue(
+ '',
+ )
+ await user.click(within(configure).getByRole('button', { name: 'common.operation.close' }))
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
+ expect(savedMethod()).toHaveTextContent(JSON.stringify(method))
})
- it('should keep actions disabled in readonly mode', async () => {
+ it('keeps actions disabled in readonly mode without changing owner state', async () => {
const user = userEvent.setup()
- const handleChange = vi.fn()
+ const method: DeliveryMethod = {
+ id: 'email-3',
+ type: DeliveryMethodType.Email,
+ enabled: true,
+ config: createEmailConfig(),
+ }
+ renderMethod(method, true)
- render(
- ,
- )
-
- await user.click(screen.getByRole('switch'))
- expect(handleChange).not.toHaveBeenCalled()
+ const enabled = screen.getByRole('switch')
+ expect(enabled).toHaveAttribute('aria-disabled', 'true')
+ await user.click(enabled)
+ expect(enabled).toBeChecked()
expect(
screen.queryByRole('button', { name: 'common.operation.remove' }),
).not.toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'workflow.common.configure' }),
).not.toBeInTheDocument()
- expect(screen.queryByTestId('email-configure-modal')).not.toBeInTheDocument()
- expect(screen.queryByTestId('test-email-sender')).not.toBeInTheDocument()
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ expect(savedMethod()).toHaveTextContent(JSON.stringify(method))
})
})
diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx
index 57987e5696a..7555ce02f9e 100644
--- a/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx
+++ b/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx
@@ -1,39 +1,24 @@
import type { LLMNodeType } from '../../types'
import type { Memory, Node, NodeOutPutVar } from '@/app/components/workflow/types'
-import { render, screen } from '@testing-library/react'
+import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
+import { replacePromptEditorText } from '@/app/components/base/prompt-editor/plugins/__tests__/test-helpers'
+import { createNode, createStartNode } from '@/app/components/workflow/__tests__/fixtures'
+import { BlockEnum } from '@/app/components/workflow/types'
+import { renderWorkflowEditor } from '@/test/workflow-editor'
import PanelMemorySection from '../panel-memory-section'
-vi.mock('@/app/components/workflow/nodes/_base/components/prompt/editor', () => ({
- default: ({
- value,
- onChange,
- readOnly,
- }: {
- value: string
- onChange: (value: string) => void
- readOnly?: boolean
- }) => (
-