From d5870d26207e60bea4360a702c0824224d91d757 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Wed, 25 Mar 2026 15:36:59 +0800 Subject: [PATCH] test: improve performance and error handling in variable modal and translation tests --- web/__tests__/check-i18n.test.ts | 4 ++-- .../use-node-resize-observer.spec.tsx | 4 ++++ .../workflow-panel/__tests__/helpers.spec.tsx | 8 ++------ .../components/workflow-panel/helpers.tsx | 9 +-------- .../__tests__/generic-table.spec.tsx | 4 +++- .../use-variable-modal-state.spec.ts | 19 +++++++++++++++++- .../__tests__/variable-modal.helpers.spec.ts | 5 +++++ .../__tests__/variable-modal.spec.tsx | 20 ++++++++++++++++++- .../components/use-variable-modal-state.ts | 11 ++++++---- .../components/variable-modal.helpers.ts | 2 +- .../components/variable-modal.sections.tsx | 5 ++++- 11 files changed, 66 insertions(+), 25 deletions(-) diff --git a/web/__tests__/check-i18n.test.ts b/web/__tests__/check-i18n.test.ts index 9e9b3d7168..de78ae997e 100644 --- a/web/__tests__/check-i18n.test.ts +++ b/web/__tests__/check-i18n.test.ts @@ -774,7 +774,7 @@ export default translation` const endTime = Date.now() expect(keys.length).toBe(1000) - expect(endTime - startTime).toBeLessThan(10000) + expect(endTime - startTime).toBeLessThan(1000) // Should complete in under 1 second }) it('should handle multiple translation files concurrently', async () => { @@ -796,7 +796,7 @@ export default translation` const endTime = Date.now() expect(keys.length).toBe(20) // 10 files * 2 keys each - expect(endTime - startTime).toBeLessThan(10000) + expect(endTime - startTime).toBeLessThan(500) }) }) diff --git a/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx b/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx index 9ee377be4d..a098ca997f 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx +++ b/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx @@ -2,6 +2,10 @@ import { renderHook } from '@testing-library/react' import useNodeResizeObserver from '../use-node-resize-observer' describe('useNodeResizeObserver', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + it('should observe and disconnect when enabled with a mounted node ref', () => { const observe = vi.fn() const disconnect = vi.fn() diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx index 5eef8d3fa4..2929d0e47e 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx @@ -75,16 +75,12 @@ describe('workflow-panel helpers', () => { }) describe('custom run form fallback', () => { - it('should return a fallback message for unsupported custom run form nodes', () => { + it('should return null for unsupported custom run form nodes', () => { const form = getCustomRunForm({ ...createCustomRunFormProps({ type: BlockEnum.Tool }), }) - expect(form).toMatchObject({ - props: { - children: expect.arrayContaining(['Custom Run Form:', ' ', 'not found']), - }, - }) + expect(form).toBeNull() }) }) }) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx index c303bdc7f0..2e8e75d2a9 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx @@ -39,14 +39,7 @@ export const getCustomRunForm = (params: CustomRunFormProps): ReactNode => { case BlockEnum.DataSource: return default: - return ( -
- Custom Run Form: - {nodeType} - {' '} - not found -
- ) + return null } } diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx index 388ca255c8..4a4d94d3c3 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx @@ -50,6 +50,7 @@ const advancedColumns = [ describe('GenericTable', () => { beforeEach(() => { vi.clearAllMocks() + vi.useRealTimers() }) it('should render an empty editable row and append a configured row when typing into the virtual row', async () => { @@ -144,10 +145,11 @@ describe('GenericTable', () => { ) await user.click(screen.getByRole('button', { name: 'Choose method' })) - await user.click(await screen.findByRole('option', { name: 'POST' })) + await user.click(await screen.findByText('POST')) await waitFor(() => { expect(onChange).toHaveBeenCalledWith([{ method: 'post', preview: '' }]) + expect(screen.getByRole('button', { name: 'POST' })).toBeInTheDocument() }) onChange.mockClear() diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts index 61ad609e50..47aeb57ae7 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts @@ -90,6 +90,23 @@ describe('useVariableModalState', () => { ]) }) + it('should keep valid object rows when switching to json mode from form mode', () => { + const { result } = renderHook(() => useVariableModalState(createOptions())) + + act(() => { + result.current.handleTypeChange(ChatVarType.Object) + result.current.setObjectValue([ + { key: '', type: ChatVarType.String, value: undefined }, + { key: 'timeout', type: ChatVarType.Number, value: 30 }, + ]) + result.current.handleEditorChange(true) + }) + + expect(result.current.editInJSON).toBe(true) + expect(result.current.value).toEqual({ timeout: 30 }) + expect(result.current.editorContent).toBe(JSON.stringify({ timeout: 30 })) + }) + it('should reset object form values when leaving empty json mode', () => { const { result } = renderHook(() => useVariableModalState(createOptions({ chatVar: { @@ -161,7 +178,7 @@ describe('useVariableModalState', () => { result.current.handleSave() }) - expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'object key can not be empty' }) + expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'chatVariable.modal.objectKeyRequired' }) expect(onSave).not.toHaveBeenCalled() expect(onClose).not.toHaveBeenCalled() }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts index 9e082265d6..2caaf9b90d 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts @@ -33,6 +33,11 @@ describe('variable-modal helpers', () => { { key: '', type: ChatVarType.Number, value: 1 }, ])).toEqual({ apiKey: 'secret' }) + expect(formatObjectValueFromList([ + { key: 'count', type: ChatVarType.Number, value: 0 }, + { key: 'label', type: ChatVarType.String, value: '' }, + ])).toEqual({ count: 0, label: null }) + expect(formatChatVariableValue({ editInJSON: false, objectValue: [{ key: 'enabled', type: ChatVarType.String, value: 'true' }], diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx index 596383cb87..e61a6bd085 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx @@ -80,7 +80,7 @@ describe('variable-modal', () => { await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'existing_name') await user.click(screen.getByText('common.operation.save')) - expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('name is existed') + expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('appDebug.varKeyError.keyAlreadyExists:{"key":"workflow.chatVariable.modal.name"}') expect(onSave).not.toHaveBeenCalled() }) @@ -195,4 +195,22 @@ describe('variable-modal', () => { description: '', }) }) + + it('should keep the number input empty while editing after the user clears it', async () => { + const user = userEvent.setup() + renderVariableModal({ + chatVar: { + id: 'var-4', + name: 'timeout', + description: '', + value_type: ChatVarType.Number, + value: 3, + }, + }) + + const input = screen.getByDisplayValue('3') as HTMLInputElement + await user.clear(input) + + expect(input.value).toBe('') + }) }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts index ecc8af6432..648fdc8eaf 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts @@ -108,7 +108,7 @@ export const useVariableModalState = ({ if (prev.type === ChatVarType.Object) { if (nextEditInJSON) { - const nextValue = !prev.objectValue[0].key ? undefined : formatObjectValueFromList(prev.objectValue) + const nextValue = prev.objectValue.some(item => item.key) ? formatObjectValueFromList(prev.objectValue) : undefined nextState.value = nextValue nextState.editorContent = JSON.stringify(nextValue) return nextState @@ -181,12 +181,15 @@ export const useVariableModalState = ({ return if (!chatVar && conversationVariables.some(item => item.name === state.name)) { - notify({ type: 'error', message: 'name is existed' }) + notify({ + type: 'error', + message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: t('chatVariable.modal.name', { ns: 'workflow' }) }), + }) return } - if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && !!item.value)) { - notify({ type: 'error', message: 'object key can not be empty' }) + if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && item.value !== undefined && item.value !== '')) { + notify({ type: 'error', message: t('chatVariable.modal.objectKeyRequired', { ns: 'workflow' }) }) return } diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts index 944b197e19..07369ed1fe 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts @@ -72,7 +72,7 @@ export const buildObjectValueItems = (chatVar?: ConversationVariable): ObjectVal export const formatObjectValueFromList = (list: ObjectValueItem[]) => { return list.reduce>((acc, curr) => { if (curr.key) - acc[curr.key] = curr.value || null + acc[curr.key] = curr.value === '' || curr.value === undefined ? null : curr.value return acc }, {}) } diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx index dd7e69bb34..6b31e024b4 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx @@ -138,7 +138,10 @@ export const ValueSection = ({ onArrayChange([Number(e.target.value)])} + onChange={(e) => { + const rawValue = e.target.value + onArrayChange([rawValue === '' ? undefined : Number(rawValue)]) + }} type="number" /> )}