mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
test(web): replace fake workflow controls with behavioral tests (#45025)
This commit is contained in:
1 parent
1fc2ed94b6
commit
e0af6d07aa
4 files changed
+540
-440
No files matched your search
+184
-146
@@ -1,163 +1,201 @@
|
||||
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { Variable } from '@/app/components/workflow/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { createEdge, createNode } from '@/app/components/workflow/__tests__/fixtures'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { renderWorkflowEditor } from '@/test/workflow-editor'
|
||||
import VarList from '../var-list'
|
||||
|
||||
vi.mock('../var-reference-picker', () => ({
|
||||
default: () => <button type="button">Variable value</button>,
|
||||
const initialVariables: Variable[] = ['alpha', 'beta', 'gamma'].map((variable) => ({
|
||||
variable,
|
||||
value_selector: ['source', variable],
|
||||
value_type: VarType.string,
|
||||
}))
|
||||
|
||||
function SortableVariables({
|
||||
initial = initialVariables,
|
||||
readonly = false,
|
||||
}: {
|
||||
initial?: Variable[]
|
||||
readonly?: boolean
|
||||
}) {
|
||||
const [variables, setVariables] = useState(initial)
|
||||
return (
|
||||
<>
|
||||
<VarList nodeId="consumer" readonly={readonly} list={variables} onChange={setVariables} />
|
||||
<output aria-label="Saved variables">{JSON.stringify(variables)}</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderVariables = (props: Parameters<typeof SortableVariables>[0] = {}) => {
|
||||
const sourceData: CodeNodeType = {
|
||||
title: 'Source',
|
||||
desc: '',
|
||||
type: BlockEnum.Code,
|
||||
code_language: CodeLanguage.javascript,
|
||||
code: 'function main() { return {} }',
|
||||
variables: [],
|
||||
outputs: Object.fromEntries(
|
||||
initialVariables.map(({ variable }) => [variable, { type: VarType.string, children: null }]),
|
||||
),
|
||||
}
|
||||
const source = createNode({
|
||||
id: 'source',
|
||||
data: sourceData,
|
||||
})
|
||||
const consumerData: CodeNodeType = {
|
||||
title: 'Consumer',
|
||||
desc: '',
|
||||
type: BlockEnum.Code,
|
||||
code_language: CodeLanguage.javascript,
|
||||
code: 'function main() { return {} }',
|
||||
variables: initialVariables,
|
||||
outputs: {},
|
||||
}
|
||||
const consumer = createNode({ id: 'consumer', data: consumerData })
|
||||
return renderWorkflowEditor(<SortableVariables {...props} />, {
|
||||
nodes: [source, consumer],
|
||||
edges: [
|
||||
createEdge({
|
||||
source: 'source',
|
||||
target: 'consumer',
|
||||
data: { sourceType: BlockEnum.Code, targetType: BlockEnum.Code },
|
||||
}),
|
||||
],
|
||||
initialStoreState: { isWorkflowDataLoaded: true },
|
||||
reactFlowProps: {
|
||||
edgesFocusable: false,
|
||||
nodesFocusable: false,
|
||||
proOptions: { hideAttribution: true },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const savedVariables = () => screen.getByRole('status', { name: 'Saved variables' })
|
||||
|
||||
describe('VarList', () => {
|
||||
it('normalizes a variable name through its labeled input', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
function EditableVariable() {
|
||||
const [list, setList] = useState<Variable[]>([
|
||||
{
|
||||
variable: 'input',
|
||||
value_selector: ['node-id', 'value'],
|
||||
value_type: VarType.string,
|
||||
},
|
||||
])
|
||||
return (
|
||||
<VarList
|
||||
nodeId="node-id"
|
||||
readonly={false}
|
||||
list={list}
|
||||
onChange={(next) => {
|
||||
setList(next)
|
||||
onChange(next)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
render(<EditableVariable />)
|
||||
renderVariables({ initial: [initialVariables[0]!] })
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.common.variableNamePlaceholder' })
|
||||
await user.clear(input)
|
||||
await user.paste('renamed input')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([expect.objectContaining({ variable: 'renamed_input' })])
|
||||
expect(savedVariables()).toHaveTextContent(
|
||||
JSON.stringify([{ ...initialVariables[0], variable: 'renamed_input' }]),
|
||||
)
|
||||
})
|
||||
|
||||
it('tabs through each variable from the reorder handle to its name, value actions and remove button', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderVariables()
|
||||
const handles = screen.getAllByRole('button', { pressed: false })
|
||||
const names = screen.getAllByRole('textbox', {
|
||||
name: 'workflow.common.variableNamePlaceholder',
|
||||
})
|
||||
const values = initialVariables.map(({ variable }) =>
|
||||
screen.getByRole('button', { name: new RegExp(`Source.*${variable}.*string`) }),
|
||||
)
|
||||
const clearButtons = screen.getAllByRole('button', { name: 'common.operation.clear' })
|
||||
const pickerFocusTargets = screen.getAllByRole('textbox', { name: '' })
|
||||
const removeButtons = screen.getAllByRole('button', { name: 'common.operation.remove' })
|
||||
|
||||
for (let index = 0; index < initialVariables.length; index++) {
|
||||
for (const control of [
|
||||
handles[index],
|
||||
names[index],
|
||||
values[index],
|
||||
clearButtons[index],
|
||||
pickerFocusTargets[index],
|
||||
removeButtons[index],
|
||||
]) {
|
||||
await user.tab()
|
||||
expect(control).toHaveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
await user.tab({ shift: true })
|
||||
expect(pickerFocusTargets[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(clearButtons[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(values[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(names[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(handles[2]).toHaveFocus()
|
||||
})
|
||||
|
||||
it('previews variable ordering, commits only on confirmation, and keeps the moved handle focused', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderVariables()
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab()
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard('{Enter}{ArrowDown}')
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('textbox', { name: 'workflow.common.variableNamePlaceholder' })
|
||||
.map((input) => (input as HTMLInputElement).value),
|
||||
).toEqual(['beta', 'alpha', 'gamma'])
|
||||
expect(savedVariables()).toHaveTextContent(JSON.stringify(initialVariables))
|
||||
expect(screen.getByRole('button', { pressed: true })).toHaveFocus()
|
||||
await user.keyboard('{Enter}')
|
||||
expect(savedVariables()).toHaveTextContent(
|
||||
JSON.stringify([initialVariables[1], initialVariables[0], initialVariables[2]]),
|
||||
)
|
||||
expect(screen.getAllByRole('button', { pressed: false })[1]).toHaveFocus()
|
||||
})
|
||||
|
||||
it('cancels a variable reorder and leaves arrow keys inside name inputs available for editing', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderVariables()
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
expect(
|
||||
screen.getAllByRole('textbox', { name: 'workflow.common.variableNamePlaceholder' })[0],
|
||||
).toHaveFocus()
|
||||
await user.keyboard('{ArrowDown}')
|
||||
expect(savedVariables()).toHaveTextContent(JSON.stringify(initialVariables))
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab({ shift: true })
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard(' {ArrowDown}{Escape}')
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('textbox', { name: 'workflow.common.variableNamePlaceholder' })
|
||||
.map((input) => (input as HTMLInputElement).value),
|
||||
).toEqual(['alpha', 'beta', 'gamma'])
|
||||
expect(savedVariables()).toHaveTextContent(JSON.stringify(initialVariables))
|
||||
expect(handle).toHaveFocus()
|
||||
})
|
||||
|
||||
it('does not expose variable sorting in readonly mode', () => {
|
||||
renderVariables({ readonly: true })
|
||||
expect(screen.queryByRole('button', { pressed: false })).not.toBeInTheDocument()
|
||||
expect(savedVariables()).toHaveTextContent(JSON.stringify(initialVariables))
|
||||
})
|
||||
|
||||
it('cancels a preview before deleting the variable whose remove button was activated', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderVariables()
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab()
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard('{Enter}{ArrowDown}')
|
||||
await user.click(screen.getAllByRole('button', { name: 'common.operation.remove' })[1]!)
|
||||
expect(savedVariables()).toHaveTextContent(
|
||||
JSON.stringify([initialVariables[1], initialVariables[2]]),
|
||||
)
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('textbox', { name: 'workflow.common.variableNamePlaceholder' })
|
||||
.map((input) => (input as HTMLInputElement).value),
|
||||
).toEqual(['beta', 'gamma'])
|
||||
})
|
||||
})
|
||||
|
||||
const initialVariables: Variable[] = ['alpha', 'beta', 'gamma'].map((variable) => ({
|
||||
variable,
|
||||
value_selector: ['node-id', variable],
|
||||
value_type: VarType.string,
|
||||
}))
|
||||
|
||||
function SortableVariables({
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
onChange: (items: Variable[]) => void
|
||||
readonly?: boolean
|
||||
}) {
|
||||
const [variables, setVariables] = useState(initialVariables)
|
||||
return (
|
||||
<VarList
|
||||
nodeId="node-id"
|
||||
readonly={readonly}
|
||||
list={variables}
|
||||
onChange={(items) => {
|
||||
onChange(items)
|
||||
setVariables(items)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
it('tabs through each variable from the reorder handle to its name, value and remove button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SortableVariables onChange={vi.fn()} />)
|
||||
const handles = screen.getAllByRole('button', { pressed: false })
|
||||
const names = screen.getAllByRole('textbox')
|
||||
const values = screen.getAllByRole('button', { name: 'Variable value' })
|
||||
const removeButtons = screen.getAllByRole('button', { name: 'common.operation.remove' })
|
||||
|
||||
for (let index = 0; index < initialVariables.length; index++) {
|
||||
for (const control of [handles[index], names[index], values[index], removeButtons[index]]) {
|
||||
await user.tab()
|
||||
expect(control).toHaveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
await user.tab({ shift: true })
|
||||
expect(values[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(names[2]).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(handles[2]).toHaveFocus()
|
||||
})
|
||||
|
||||
it('previews variable ordering, commits only on confirmation, and keeps the moved handle focused', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<SortableVariables onChange={onChange} />)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab()
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard('{Enter}{ArrowDown}')
|
||||
expect(screen.getAllByRole('textbox').map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
'beta',
|
||||
'alpha',
|
||||
'gamma',
|
||||
])
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('button', { pressed: true })).toHaveFocus()
|
||||
await user.keyboard('{Enter}')
|
||||
expect(onChange).toHaveBeenCalledExactlyOnceWith([
|
||||
initialVariables[1],
|
||||
initialVariables[0],
|
||||
initialVariables[2],
|
||||
])
|
||||
expect(screen.getAllByRole('button', { pressed: false })[1]).toHaveFocus()
|
||||
})
|
||||
|
||||
it('cancels a variable reorder and leaves arrow keys inside name inputs available for editing', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<SortableVariables onChange={onChange} />)
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
expect(screen.getAllByRole('textbox')[0]).toHaveFocus()
|
||||
await user.keyboard('{ArrowDown}')
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab({ shift: true })
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard(' {ArrowDown}{Escape}')
|
||||
expect(screen.getAllByRole('textbox').map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
'alpha',
|
||||
'beta',
|
||||
'gamma',
|
||||
])
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(handle).toHaveFocus()
|
||||
})
|
||||
|
||||
it('does not expose variable sorting in readonly mode', () => {
|
||||
render(<SortableVariables onChange={vi.fn()} readonly />)
|
||||
expect(screen.queryByRole('button', { pressed: false })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cancels a preview before deleting the variable whose remove button was activated', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<SortableVariables onChange={onChange} />)
|
||||
const handle = screen.getAllByRole('button', { pressed: false })[0]!
|
||||
await user.tab()
|
||||
expect(handle).toHaveFocus()
|
||||
await user.keyboard('{Enter}{ArrowDown}')
|
||||
await user.click(screen.getAllByRole('button', { name: 'common.operation.remove' })[1]!)
|
||||
expect(onChange).toHaveBeenCalledExactlyOnceWith([initialVariables[1], initialVariables[2]])
|
||||
expect(screen.getAllByRole('textbox').map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
'beta',
|
||||
'gamma',
|
||||
])
|
||||
})
|
||||
+191
-195
@@ -1,77 +1,49 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { EmailConfig, FormInputItem } from '../../../types'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { DeliveryMethod, EmailConfig, FormInputItem, HumanInputNodeType } from '../../../types'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
|
||||
import { render as renderWithConsoleState } from '@/test/console/render'
|
||||
import { useState } from 'react'
|
||||
import { replacePromptEditorText } from '@/app/components/base/prompt-editor/plugins/__tests__/test-helpers'
|
||||
import {
|
||||
createEdge,
|
||||
createNode,
|
||||
createStartNode,
|
||||
} from '@/app/components/workflow/__tests__/fixtures'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import { renderWorkflowEditor } from '@/test/workflow-editor'
|
||||
import { DeliveryMethodType } from '../../../types'
|
||||
import DeliveryMethodItem from '../method-item'
|
||||
|
||||
type EmailConfigureModalProps = {
|
||||
open: boolean
|
||||
config?: EmailConfig
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (data: EmailConfig) => void
|
||||
}
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'workspaces.current.members.get': () => ({ accounts: [] }),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
type TestEmailSenderProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
jumpToEmailConfigModal: () => void
|
||||
}
|
||||
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
}))
|
||||
|
||||
const render = (ui: ReactElement) =>
|
||||
renderWithConsoleState(ui, {
|
||||
wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile),
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
const { createInstance } = await vi.importActual<typeof import('i18next')>('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 ? (
|
||||
<div data-testid="email-configure-modal">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
props.onConfirm({
|
||||
recipients: { whole_workspace: false, items: [] },
|
||||
subject: 'Configured subject',
|
||||
body: '{{#url#}}',
|
||||
debug_mode: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
confirm-email-config
|
||||
</button>
|
||||
<button type="button" onClick={() => props.onOpenChange(false)}>
|
||||
close-email-config
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../test-email-sender', () => ({
|
||||
default: (props: TestEmailSenderProps) => {
|
||||
return props.open ? (
|
||||
<div data-testid="test-email-sender">
|
||||
<button type="button" onClick={props.jumpToEmailConfigModal}>
|
||||
jump-to-config
|
||||
</button>
|
||||
<button type="button" onClick={() => props.onOpenChange(false)}>
|
||||
close-test-sender
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
},
|
||||
}))
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock(),
|
||||
Trans: (props: ComponentProps<typeof actual.Trans>) => <actual.Trans {...props} i18n={i18n} />,
|
||||
}
|
||||
})
|
||||
|
||||
const createEmailConfig = (overrides: Partial<EmailConfig> = {}): 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<DeliveryMethod | null>(initialMethod)
|
||||
return (
|
||||
<>
|
||||
{method && (
|
||||
<DeliveryMethodItem
|
||||
nodeId="human-input"
|
||||
method={method}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
formContent="Hello {{#url#}}"
|
||||
formInputs={formInputs}
|
||||
onChange={setMethod}
|
||||
onDelete={(type) => setMethod((current) => (current?.type === type ? null : current))}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
<output aria-label="Saved delivery method">
|
||||
{method ? JSON.stringify(method) : 'removed'}
|
||||
</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
<DeliveryMethodHarness
|
||||
availableNodes={[start]}
|
||||
initialMethod={initialMethod}
|
||||
readonly={readonly}
|
||||
/>,
|
||||
{
|
||||
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(
|
||||
<DeliveryMethodItem
|
||||
nodeId="node-1"
|
||||
method={{
|
||||
id: 'webapp-1',
|
||||
type: DeliveryMethodType.WebApp,
|
||||
enabled: true,
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onDelete={handleDelete}
|
||||
/>,
|
||||
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(
|
||||
<DeliveryMethodItem
|
||||
nodeId="node-1"
|
||||
method={{
|
||||
id: 'email-1',
|
||||
type: DeliveryMethodType.Email,
|
||||
enabled: true,
|
||||
config: createEmailConfig({ debug_mode: true }),
|
||||
}}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
formContent="Hello {{#url#}}"
|
||||
formInputs={formInputs}
|
||||
onChange={handleChange}
|
||||
onDelete={handleDelete}
|
||||
/>,
|
||||
)
|
||||
|
||||
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(
|
||||
<DeliveryMethodItem
|
||||
nodeId="node-1"
|
||||
method={{
|
||||
id: 'email-2',
|
||||
type: DeliveryMethodType.Email,
|
||||
enabled: false,
|
||||
config: undefined,
|
||||
}}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
formInputs={formInputs}
|
||||
onChange={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
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(
|
||||
<DeliveryMethodItem
|
||||
nodeId="node-1"
|
||||
method={{
|
||||
id: 'email-3',
|
||||
type: DeliveryMethodType.Email,
|
||||
enabled: true,
|
||||
config: createEmailConfig(),
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onDelete={vi.fn()}
|
||||
readonly
|
||||
/>,
|
||||
)
|
||||
|
||||
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))
|
||||
})
|
||||
})
|
||||
+115
-83
@@ -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
|
||||
}) => (
|
||||
<textarea
|
||||
aria-label="Memory prompt"
|
||||
value={value}
|
||||
readOnly={readOnly}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
const createInputs = (overrides: Partial<LLMNodeType> = {}): LLMNodeType => ({
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
type: 'llm' as LLMNodeType['type'],
|
||||
type: BlockEnum.LLM,
|
||||
model: {
|
||||
provider: 'openai',
|
||||
name: 'gpt-4o',
|
||||
mode: 'chat' as const,
|
||||
mode: 'chat',
|
||||
completion_params: {},
|
||||
} as LLMNodeType['model'],
|
||||
},
|
||||
prompt_template: [],
|
||||
context: {
|
||||
enabled: false,
|
||||
@@ -52,60 +37,110 @@ const createInputs = (overrides: Partial<LLMNodeType> = {}): LLMNodeType => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const baseProps = {
|
||||
readOnly: false,
|
||||
isChatflow: true,
|
||||
isChatModel: true,
|
||||
isCompletionModel: false,
|
||||
inputs: createInputs(),
|
||||
hasSetBlockStatus: {
|
||||
history: false,
|
||||
query: true,
|
||||
context: false,
|
||||
},
|
||||
availableVars: [] as NodeOutPutVar[],
|
||||
availableNodesWithParent: [] as Node[],
|
||||
handleSyeQueryChange: vi.fn(),
|
||||
handleMemoryChange: vi.fn(),
|
||||
type HarnessProps = {
|
||||
availableNodesWithParent: Node[]
|
||||
availableVars: NodeOutPutVar[]
|
||||
initialMemory?: Memory
|
||||
isChatflow?: boolean
|
||||
readOnly?: boolean
|
||||
resourceKind?: 'app' | 'snippet'
|
||||
}
|
||||
|
||||
function MemoryHarness({
|
||||
availableNodesWithParent,
|
||||
availableVars,
|
||||
initialMemory,
|
||||
isChatflow = true,
|
||||
readOnly = false,
|
||||
resourceKind = 'app',
|
||||
}: HarnessProps) {
|
||||
const [inputs, setInputs] = useState(() => createInputs({ memory: initialMemory }))
|
||||
return (
|
||||
<>
|
||||
<PanelMemorySection
|
||||
readOnly={readOnly}
|
||||
isChatflow={isChatflow}
|
||||
isChatModel
|
||||
isCompletionModel={false}
|
||||
inputs={inputs}
|
||||
hasSetBlockStatus={{ history: false, query: true, context: false }}
|
||||
availableVars={availableVars}
|
||||
availableNodesWithParent={availableNodesWithParent}
|
||||
resourceKind={resourceKind}
|
||||
handleMemoryChange={(memory) => setInputs((current) => ({ ...current, memory }))}
|
||||
handleSyeQueryChange={(query) =>
|
||||
setInputs((current) => ({
|
||||
...current,
|
||||
memory: { ...current.memory!, query_prompt_template: query },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<output aria-label="Saved memory">
|
||||
{inputs.memory ? JSON.stringify(inputs.memory) : 'unset'}
|
||||
</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderMemory = (props: Omit<HarnessProps, 'availableNodesWithParent' | 'availableVars'>) => {
|
||||
const start = createStartNode({ id: 'start' })
|
||||
const llm = createNode({ id: 'llm', data: createInputs({ memory: props.initialMemory }) })
|
||||
return renderWorkflowEditor(
|
||||
<MemoryHarness {...props} availableNodesWithParent={[start]} availableVars={[]} />,
|
||||
{
|
||||
nodes: [start, llm],
|
||||
resource:
|
||||
props.resourceKind === 'snippet'
|
||||
? { kind: 'snippet', snippetId: 'snippet-1' }
|
||||
: { kind: 'app', appId: 'app-1', mode: 'advanced-chat' },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const savedMemory = () => screen.getByRole('status', { name: 'Saved memory' })
|
||||
|
||||
it.each([
|
||||
{ resourceKind: 'app' as const, expected: '{{#sys.query#}}\n\n{{#sys.files#}}' },
|
||||
{ resourceKind: 'snippet' as const, expected: '' },
|
||||
])(
|
||||
'enables memory with the correct $resourceKind prompt and saves edits',
|
||||
'enables memory with the correct $resourceKind prompt and saves real editor edits',
|
||||
async ({ resourceKind, expected }) => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
function Harness() {
|
||||
const [memory, setMemory] = useState<Memory>()
|
||||
return (
|
||||
<PanelMemorySection
|
||||
{...baseProps}
|
||||
resourceKind={resourceKind}
|
||||
inputs={createInputs({ memory })}
|
||||
handleMemoryChange={(next) => {
|
||||
setMemory(next)
|
||||
onChange(next)
|
||||
}}
|
||||
handleSyeQueryChange={(query) =>
|
||||
setMemory((previous) => ({ ...previous!, query_prompt_template: query }))
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
render(<Harness />)
|
||||
renderMemory({ resourceKind })
|
||||
await user.click(screen.getByRole('switch'))
|
||||
expect(onChange).toHaveBeenCalledExactlyOnceWith({
|
||||
window: { enabled: false, size: 50 },
|
||||
query_prompt_template: expected,
|
||||
})
|
||||
expect(screen.getByRole('textbox', { name: 'Memory prompt' })).toHaveValue(expected)
|
||||
const prompt = screen.getByRole('textbox', { name: 'Memory prompt' })
|
||||
await user.clear(prompt)
|
||||
expect(prompt).toHaveValue('')
|
||||
await user.type(prompt, 'Custom prompt')
|
||||
expect(screen.getByRole('textbox', { name: 'Memory prompt' })).toHaveValue('Custom prompt')
|
||||
expect(savedMemory()).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
window: { enabled: false, size: 50 },
|
||||
query_prompt_template: expected,
|
||||
}),
|
||||
)
|
||||
|
||||
// This composition does not connect the visible "user" heading to the editor. The Lexical
|
||||
// contenteditable is the sole editable textbox; MemoryConfig also exposes a disabled textbox.
|
||||
const editableTextboxes = screen
|
||||
.getAllByRole('textbox')
|
||||
.filter((element) => element.getAttribute('contenteditable') === 'true')
|
||||
expect(editableTextboxes).toHaveLength(1)
|
||||
const prompt = editableTextboxes[0]!
|
||||
await replacePromptEditorText(prompt, '')
|
||||
await waitFor(() =>
|
||||
expect(savedMemory()).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
window: { enabled: false, size: 50 },
|
||||
query_prompt_template: '',
|
||||
}),
|
||||
),
|
||||
)
|
||||
await replacePromptEditorText(prompt, 'Custom prompt')
|
||||
await waitFor(() =>
|
||||
expect(savedMemory()).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
window: { enabled: false, size: 50 },
|
||||
query_prompt_template: 'Custom prompt',
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(prompt).toHaveTextContent('Custom prompt')
|
||||
if (resourceKind === 'app')
|
||||
expect(screen.getByText('workflow.nodes.llm.sysQueryInUser')).toBeVisible()
|
||||
else expect(screen.queryByText('workflow.nodes.llm.sysQueryInUser')).not.toBeInTheDocument()
|
||||
@@ -114,24 +149,21 @@ it.each([
|
||||
|
||||
it('prevents memory activation when read-only', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<PanelMemorySection
|
||||
{...baseProps}
|
||||
readOnly
|
||||
inputs={createInputs({ memory: undefined })}
|
||||
handleMemoryChange={onChange}
|
||||
/>,
|
||||
)
|
||||
await user.click(screen.getByRole('switch'))
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('textbox', { name: 'Memory prompt' })).not.toBeInTheDocument()
|
||||
renderMemory({ readOnly: true })
|
||||
const memorySwitch = screen.getByRole('switch')
|
||||
expect(memorySwitch).toHaveAttribute('aria-disabled', 'true')
|
||||
await user.click(memorySwitch)
|
||||
expect(savedMemory()).toHaveTextContent('unset')
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides memory settings outside chatflow', () => {
|
||||
render(
|
||||
<PanelMemorySection {...baseProps} isChatflow={false} isChatModel={false} isCompletionModel />,
|
||||
)
|
||||
const memory: Memory = {
|
||||
window: { enabled: false, size: 10 },
|
||||
query_prompt_template: 'Existing prompt',
|
||||
}
|
||||
renderMemory({ initialMemory: memory, isChatflow: false })
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
expect(savedMemory()).toHaveTextContent(JSON.stringify(memory))
|
||||
})
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { InputVar } 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 { InputVarType } from '@/app/components/workflow/types'
|
||||
import { renderWorkflowEditor } from '@/test/workflow-editor'
|
||||
import VarItem from '../var-item'
|
||||
|
||||
vi.mock('@/app/components/app/configuration/config-var/config-modal', () => ({
|
||||
default: ({ isShow, onClose }: { isShow: boolean; onClose: () => void }) =>
|
||||
isShow ? (
|
||||
<div role="dialog">
|
||||
edit-variable<button onClick={onClose}>Close editor</button>
|
||||
</div>
|
||||
) : null,
|
||||
vi.mock('@/next/navigation', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/next/navigation')>()),
|
||||
useParams: () => ({ appId: 'app-1' }),
|
||||
}))
|
||||
|
||||
const createPayload = (overrides: Partial<InputVar> = {}): InputVar => ({
|
||||
@@ -21,21 +19,57 @@ const createPayload = (overrides: Partial<InputVar> = {}): InputVar => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
function VarItemHarness() {
|
||||
const [payload, setPayload] = useState<InputVar | null>(() => createPayload())
|
||||
return (
|
||||
<>
|
||||
{payload && (
|
||||
<VarItem
|
||||
readonly={false}
|
||||
payload={payload}
|
||||
onChange={(next) => {
|
||||
setPayload(next)
|
||||
return true
|
||||
}}
|
||||
onRemove={() => setPayload(null)}
|
||||
/>
|
||||
)}
|
||||
<output aria-label="Saved start variable">
|
||||
{payload ? JSON.stringify(payload) : 'removed'}
|
||||
</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('StartVarItem', () => {
|
||||
it('opens the editor and removes the variable from its hover actions', async () => {
|
||||
it('opens and dismisses the prepopulated editor, then removes the variable from owner state', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleRemove = vi.fn()
|
||||
render(<VarItem readonly={false} payload={createPayload()} onRemove={handleRemove} />)
|
||||
renderWorkflowEditor(<VarItemHarness />)
|
||||
|
||||
await user.hover(screen.getByText('query'))
|
||||
const edit = screen.getByRole('button', { name: 'common.operation.edit' })
|
||||
await user.click(edit)
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'appDebug.variableConfig.editModalTitle' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'appDebug.variableConfig.varName' })).toHaveValue(
|
||||
'query',
|
||||
)
|
||||
expect(screen.getByRole('textbox', { name: 'appDebug.variableConfig.labelName' })).toHaveValue(
|
||||
'Query',
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
expect(screen.getByRole('dialog')).toHaveTextContent('edit-variable')
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(screen.getByRole('status', { name: 'Saved start variable' })).toHaveTextContent(
|
||||
JSON.stringify(createPayload()),
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Close editor' }))
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
await user.hover(screen.getByText('query'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.remove' }))
|
||||
expect(handleRemove).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('status', { name: 'Saved start variable' })).toHaveTextContent(
|
||||
'removed',
|
||||
)
|
||||
expect(screen.queryByText('query')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user