From f762c59fb353dd334e4239b545ae44503b6e78d4 Mon Sep 17 00:00:00 2001 From: jeanpaul Date: Fri, 23 May 2025 13:15:09 +0200 Subject: [PATCH] fix(editor): Fix schema view showing incorrect data on loop node done branch (#15635) --- .../src/components/VirtualSchema.test.ts | 132 +++++++++++++++++- .../src/components/VirtualSchema.vue | 11 +- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts index cb0173eacd1..aa54a463d7c 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts @@ -7,7 +7,12 @@ import { createComponentRenderer } from '@/__tests__/render'; import VirtualSchema from '@/components/VirtualSchema.vue'; import * as nodeHelpers from '@/composables/useNodeHelpers'; import { useTelemetry } from '@/composables/useTelemetry'; -import { IF_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants'; +import { + IF_NODE_TYPE, + MANUAL_TRIGGER_NODE_TYPE, + SET_NODE_TYPE, + SPLIT_IN_BATCHES_NODE_TYPE, +} from '@/constants'; import type { IWorkflowDb } from '@/Interface'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -78,6 +83,20 @@ const unknownNodeType = createTestNode({ type: 'unknown', }); +const splitInBatchesNode = createTestNode({ + name: 'SplitInBatches', + type: SPLIT_IN_BATCHES_NODE_TYPE, + typeVersion: 1, + disabled: false, +}); + +const customerDatastoreNode = createTestNode({ + name: 'Customer Datastore', + type: 'n8n-nodes-base.n8nTrainingCustomerDatastore', + typeVersion: 1, + disabled: false, +}); + const defaultNodes = [ { name: 'Manual Trigger', indicies: [], depth: 1 }, { name: 'Set2', indicies: [], depth: 2 }, @@ -97,6 +116,8 @@ async function setupStore() { aiTool, unknownNodeType, nodeWithCredential, + splitInBatchesNode, + customerDatastoreNode, ], }; @@ -123,6 +144,14 @@ async function setupStore() { name: 'n8n-nodes-base.notion', outputs: [NodeConnectionTypes.Main], }), + mockNodeTypeDescription({ + name: SPLIT_IN_BATCHES_NODE_TYPE, + outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main], + }), + mockNodeTypeDescription({ + name: 'n8n-nodes-base.n8nTrainingCustomerDatastore', + outputs: [NodeConnectionTypes.Main], + }), ]); workflowsStore.workflow = workflow as IWorkflowDb; ndvStore.activeNodeName = 'Test Node Name'; @@ -356,6 +385,42 @@ describe('VirtualSchema.vue', () => { }); }); + it('renders schema for specific output branch when outputIndex is specified', async () => { + const originalNodeHelpers = nodeHelpers.useNodeHelpers(); + vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => { + return { + ...originalNodeHelpers, + getLastRunIndexWithData: vi.fn(() => 0), + hasNodeExecuted: vi.fn(() => true), + getNodeInputData: vi.fn((node, _, outputIndex) => { + if (node.name === 'If' && outputIndex === 1) { + return [{ json: { doneItems: 'done branch data' } }]; + } + if (node.name === 'If' && outputIndex === 0) { + return [{ json: { loopItems: 'loop branch data' } }]; + } + return []; + }), + }; + }); + + const { getAllByTestId } = renderComponent({ + props: { + nodes: [{ name: 'If', indicies: [0, 1], depth: 2 }], + outputIndex: 1, + }, + }); + + await waitFor(() => { + const headers = getAllByTestId('run-data-schema-header'); + expect(headers[0]).toHaveTextContent('If'); + expect(headers[0]).toHaveTextContent('1 item'); + + const items = getAllByTestId('run-data-schema-item'); + expect(items[0]).toHaveTextContent('doneItemsdone branch data'); + }); + }); + it('renders previous nodes schema for AI tools', async () => { mockNodeOutputData( 'If', @@ -680,4 +745,69 @@ describe('VirtualSchema.vue', () => { }); expect(container).toMatchSnapshot(); }); + + it('renders schema for loop node done-branch with correct filtering', async () => { + // Mock customer datastore output - 6 customer items + const customerData = Array.from({ length: 6 }, (_, i) => ({ + json: { + id: i + 1, + name: `Customer ${i + 1}`, + email: `customer${i + 1}@example.com`, + status: 'active', + }, + })); + + // Mock SplitInBatches node processing the loop with multiple items on output 0 (loop branch) + // and final completion signal on output 1 (done branch) + const originalNodeHelpers = nodeHelpers.useNodeHelpers(); + vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => { + return { + ...originalNodeHelpers, + getLastRunIndexWithData: vi.fn(() => 0), + hasNodeExecuted: vi.fn(() => true), + getNodeInputData: vi.fn((node, _, outputIndex) => { + if (node.name === 'Customer Datastore') { + return customerData; + } + if (node.name === 'SplitInBatches' && outputIndex === 0) { + // Loop branch: return individual customer items processed one by one + return customerData; // Multiple items being processed + } + if (node.name === 'SplitInBatches' && outputIndex === 1) { + // Done branch: return completion signal with aggregated results + return [ + { json: { processed: 6, completed: true, summary: 'All customers processed' } }, + ]; + } + return []; + }), + }; + }); + + // Test the done branch (outputIndex: 1) specifically + const { getAllByTestId } = renderComponent({ + props: { + nodes: [ + { name: 'Customer Datastore', indicies: [], depth: 1 }, + { name: 'SplitInBatches', indicies: [0, 1], depth: 2 }, + ], + outputIndex: 1, // Specifically viewing the done branch + }, + }); + + await waitFor(() => { + const headers = getAllByTestId('run-data-schema-header'); + expect(headers).toHaveLength(3); // Customer Datastore, SplitInBatches, and Variables + + // Check Customer Datastore (first header) + expect(headers[0]).toHaveTextContent('Customer Datastore'); + + // Check SplitInBatches shows only 1 item from the done branch (not 6 from loop branch) + expect(headers[1]).toHaveTextContent('SplitInBatches'); + expect(headers[1]).toHaveTextContent('1 item'); + + // This is the key assertion: the SplitInBatches node shows "1 item" instead of "6 items" + // which proves that the outputIndex filtering is working correctly for the done branch + }); + }); }); diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.vue b/packages/frontend/editor-ui/src/components/VirtualSchema.vue index 812211b1f3f..c59163aacaf 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.vue +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.vue @@ -55,6 +55,7 @@ type Props = { connectionType?: NodeConnectionType; search?: string; compact?: boolean; + outputIndex?: number; }; const props = withDefaults(defineProps(), { @@ -66,6 +67,7 @@ const props = withDefaults(defineProps(), { search: '', mappingEnabled: false, compact: false, + outputIndex: undefined, }); const telemetry = useTelemetry(); @@ -113,7 +115,14 @@ const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) = runIndex: getLastRunIndexWithData(fullNode.name, outputIndex, props.connectionType), })) .filter(({ runIndex }) => runIndex !== -1); - const nodeData = connectedOutputsWithData + + // If outputIndex is specified, only use data from that specific output branch + const filteredOutputsWithData = + props.outputIndex !== undefined + ? connectedOutputsWithData.filter(({ outputIndex }) => outputIndex === props.outputIndex) + : connectedOutputsWithData; + + const nodeData = filteredOutputsWithData .map(({ outputIndex, runIndex }) => getNodeInputData(fullNode, runIndex, outputIndex, props.paneType, props.connectionType), )