import { NodeConnectionTypes, type INodeType, type NodeConnectionType, type INodeIssues, type INode, type INodeParameters, type INodeProperties, type INodeTypeDescription, type NodeParameterValueType, } from '../src/interfaces'; import { getNodeParameters, isSubNodeType, getParameterIssues, isTriggerNode, isExecutable, displayParameter, makeDescription, getUpdatedToolDescription, getToolDescriptionForNode, isDefaultNodeName, makeNodeName, isTool, getNodeWebhookPath, isToolType, isHitlToolType, getNodeOutputs, nodeIssuesToString, findDisplayedProperty, isTriggerNodeType, } from '../src/node-helpers'; import type { Workflow } from '../src/workflow'; import { mock } from 'vitest-mock-extended'; describe('NodeHelpers', () => { describe('getNodeParameters', () => { const tests: Array<{ description: string; input: { nodePropertiesArray: INodeProperties[]; nodeValues: INodeParameters | null; }; output: { noneDisplayedFalse: { defaultsFalse: INodeParameters; defaultsTrue: INodeParameters; }; noneDisplayedTrue: { defaultsFalse: INodeParameters; defaultsTrue: INodeParameters; }; }; }> = [ { description: 'simple values.', input: { nodePropertiesArray: [ { name: 'string1', displayName: 'String 1', type: 'string', default: '', }, { name: 'string2', displayName: 'String 2', type: 'string', default: 'default string 2', }, { name: 'string3', displayName: 'String 3', type: 'string', default: 'default string 3', }, { name: 'number1', displayName: 'Number 1', type: 'number', default: 10, }, { name: 'number2', displayName: 'Number 2', type: 'number', default: 10, }, { name: 'number3', displayName: 'Number 3', type: 'number', default: 10, }, { name: 'boolean1', displayName: 'Boolean 1', type: 'boolean', default: false, }, { name: 'boolean2', displayName: 'Boolean 2', type: 'boolean', default: false, }, { name: 'boolean3', displayName: 'Boolean 3', type: 'boolean', default: true, }, { name: 'boolean4', displayName: 'Boolean 4', type: 'boolean', default: true, }, { name: 'boolean5', displayName: 'Boolean 5', type: 'boolean', default: false, }, { name: 'boolean6', displayName: 'Boolean 6', type: 'boolean', default: true, }, ], nodeValues: { boolean1: true, boolean3: false, boolean5: false, boolean6: true, string1: 'different', number1: 1, number3: 0, }, }, output: { noneDisplayedFalse: { defaultsFalse: { boolean1: true, boolean3: false, string1: 'different', number1: 1, number3: 0, }, defaultsTrue: { boolean1: true, boolean2: false, boolean3: false, boolean4: true, boolean5: false, boolean6: true, string1: 'different', string2: 'default string 2', string3: 'default string 3', number1: 1, number2: 10, number3: 0, }, }, noneDisplayedTrue: { defaultsFalse: { boolean1: true, boolean3: false, string1: 'different', number1: 1, number3: 0, }, defaultsTrue: { boolean1: true, boolean2: false, boolean3: false, boolean4: true, boolean5: false, boolean6: true, string1: 'different', string2: 'default string 2', string3: 'default string 3', number1: 1, number2: 10, number3: 0, }, }, }, }, { description: 'simple values with displayOptions "show" (match) which is boolean. All values set.', input: { nodePropertiesArray: [ { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, { name: 'string1', displayName: 'string1', displayOptions: { show: { boolean1: [true], }, }, type: 'string', default: 'default string1', }, ], nodeValues: { boolean1: true, string1: 'own string1', }, }, output: { noneDisplayedFalse: { defaultsFalse: { boolean1: true, string1: 'own string1', }, defaultsTrue: { boolean1: true, string1: 'own string1', }, }, noneDisplayedTrue: { defaultsFalse: { boolean1: true, string1: 'own string1', }, defaultsTrue: { boolean1: true, string1: 'own string1', }, }, }, }, { description: 'simple values with displayOptions "hide" (match) which is boolean. All values set.', input: { nodePropertiesArray: [ { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, { name: 'string1', displayName: 'string1', displayOptions: { hide: { boolean1: [true], }, }, type: 'string', default: 'default string1', }, ], nodeValues: { boolean1: true, }, }, output: { noneDisplayedFalse: { defaultsFalse: { boolean1: true, }, defaultsTrue: { boolean1: true, }, }, noneDisplayedTrue: { defaultsFalse: { boolean1: true, }, defaultsTrue: { boolean1: true, string1: 'default string1', }, }, }, }, { description: 'simple values with displayOptions "show" (match) which is boolean. One values set.', input: { nodePropertiesArray: [ { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, { name: 'string1', displayName: 'string1', displayOptions: { show: { boolean1: [true], }, }, type: 'string', default: 'default string1', }, ], nodeValues: { boolean1: true, }, }, output: { noneDisplayedFalse: { defaultsFalse: { boolean1: true, }, defaultsTrue: { boolean1: true, string1: 'default string1', }, }, noneDisplayedTrue: { defaultsFalse: { boolean1: true, }, defaultsTrue: { boolean1: true, string1: 'default string1', }, }, }, }, { description: 'simple values with displayOptions "show" (match). No values set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'string1', displayName: 'string1', displayOptions: { show: { mode: ['mode1'], }, }, type: 'string', default: 'default string1', }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, }, }, { description: 'simple values with displayOptions "show" (match) on two which depend on each other of which is boolean. One value should be displayed. One values set (none-default).', input: { nodePropertiesArray: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', displayOptions: { show: { string1: ['default string1'], }, }, type: 'boolean', default: false, }, { name: 'string2', displayName: 'string2', displayOptions: { show: { boolean1: [true], }, }, type: 'string', default: 'default string2', }, ], nodeValues: { boolean1: true, }, }, output: { noneDisplayedFalse: { defaultsFalse: { boolean1: true, }, defaultsTrue: { string1: 'default string1', boolean1: true, string2: 'default string2', }, }, noneDisplayedTrue: { defaultsFalse: { boolean1: true, }, defaultsTrue: { string1: 'default string1', boolean1: true, string2: 'default string2', }, }, }, }, { description: 'simple values with displayOptions "show" (match) on two which depend on each other of which is boolean. One value should be displayed. One values set. (default)', input: { nodePropertiesArray: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', displayOptions: { show: { string1: ['default string1'], }, }, type: 'boolean', default: false, }, { name: 'string2', displayName: 'string2', displayOptions: { show: { boolean1: [true], }, }, type: 'string', default: 'default string2', }, ], nodeValues: { boolean1: false, }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { string1: 'default string1', boolean1: false, }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { string1: 'default string1', boolean1: false, string2: 'default string2', }, }, }, }, { description: 'simple values with displayOptions "show" (match). All values set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'string1', displayName: 'string1', displayOptions: { show: { mode: ['mode1'], }, }, type: 'string', default: 'default string1', }, ], nodeValues: { mode: 'mode1', string1: 'default string1', }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, }, }, { description: 'simple values with displayOptions "show" (no-match). No values set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'string1', displayName: 'string1', displayOptions: { show: { mode: ['mode2'], }, }, type: 'string', default: 'default string1', }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, }, }, { description: 'simple values with displayOptions "show" (no-match). All values set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'string1', displayName: 'string1', displayOptions: { show: { mode: ['mode2'], }, }, type: 'string', default: 'default string1', }, ], nodeValues: { mode: 'mode1', string1: 'default string1', }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', string1: 'default string1', }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". One value set.', input: { nodePropertiesArray: [ { name: 'values', displayName: 'Values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'boolean', displayName: 'Boolean', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, ], }, { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { displayName: 'number1', name: 'number1', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number: [ { number1: 1, }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: [ { number1: 1, }, ], }, }, defaultsTrue: { values: { number: [ { string1: 'default string1', number1: 1, }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: [ { number1: 1, }, ], }, }, defaultsTrue: { values: { number: [ { string1: 'default string1', number1: 1, }, ], }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false". One value set.', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', default: {}, options: [ { name: 'boolean', displayName: 'Boolean', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, ], }, { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { displayName: 'number1', name: 'number1', type: 'number', default: 0, }, ], }, { name: 'singleString', displayName: 'Single String', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default singleString1', }, ], }, ], }, ], nodeValues: { values: { number: { number1: 1, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: { number1: 1, }, }, }, defaultsTrue: { values: { number: { string1: 'default string1', number1: 1, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { number1: 1, }, }, }, defaultsTrue: { values: { number: { string1: 'default string1', number1: 1, }, }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false". Two values set one single one.', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', default: {}, options: [ { name: 'boolean', displayName: 'Boolean', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, ], }, { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { displayName: 'number1', name: 'number1', type: 'number', default: 0, }, ], }, { name: 'singleString', displayName: 'Single String', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default singleString1', }, ], }, ], }, ], nodeValues: { values: { number: { number1: 1, }, singleString: { string1: 'value1', }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: { number1: 1, }, singleString: { string1: 'value1', }, }, }, defaultsTrue: { values: { number: { string1: 'default string1', number1: 1, }, singleString: { string1: 'value1', }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { number1: 1, }, singleString: { string1: 'value1', }, }, }, defaultsTrue: { values: { number: { string1: 'default string1', number1: 1, }, singleString: { string1: 'value1', }, }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true" and complex type "collection" with "multipleValues: true". One value set each.', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, description: 'The value to set.', default: {}, options: [ { name: 'boolean', displayName: 'Boolean', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'boolean1', displayName: 'boolean1', type: 'boolean', default: false, }, ], }, { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, { name: 'collection1', displayName: 'collection1', type: 'collection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'string2', displayName: 'string2', type: 'string', default: 'default string2', }, ], }, ], }, ], }, ], nodeValues: { values: { number: [ { number1: 1, collection1: [ { string1: 'value1', }, ], }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: [ { number1: 1, collection1: [ { string1: 'value1', }, ], }, ], }, }, defaultsTrue: { values: { number: [ { string1: 'default string1', number1: 1, collection1: [ { string1: 'value1', }, ], }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: [ { number1: 1, collection1: [ { string1: 'value1', }, ], }, ], }, }, defaultsTrue: { values: { number: [ { string1: 'default string1', number1: 1, collection1: [ { string1: 'value1', }, ], }, ], }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false" and with displayOptions "show" (match) on option. One value set.', input: { nodePropertiesArray: [ { name: 'values', displayName: 'Values', type: 'fixedCollection', default: {}, options: [ { name: 'number', displayName: 'Number', values: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { mode: ['mode1'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number: { number1: 1, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: { number1: 1, }, }, }, defaultsTrue: { values: { number: { mode: 'mode1', string1: 'default string1', number1: 1, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { number1: 1, }, }, }, defaultsTrue: { values: { number: { mode: 'mode1', string1: 'default string1', number1: 1, }, }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false" and with displayOptions "show" (match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'fixedCollection', default: {}, options: [ { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode1'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number: { string1: 'own string1', }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: { string1: 'own string1', }, }, }, defaultsTrue: { mode: 'mode1', values: { number: { string1: 'own string1', number1: 0, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { string1: 'own string1', }, }, }, defaultsTrue: { mode: 'mode1', values: { number: { string1: 'own string1', number1: 0, }, }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false" and with displayOptions "show" (no-match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'fixedCollection', default: {}, options: [ { name: 'number', displayName: 'Number', values: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode2'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number: { string1: 'own string1', }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mode: 'mode1', values: { number: { number1: 0, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { string1: 'own string1', }, }, }, defaultsTrue: { mode: 'mode1', values: { number: { string1: 'own string1', number1: 0, }, }, }, }, }, }, // Remember it is correct that default strings get returned here even when returnDefaults // is set to false because if they would not, there would be no way to know which value // got added and which one not. { description: 'complex type "collection" with "multipleValues: false" and with displayOptions "show" (match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'collection', default: {}, options: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode1'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], nodeValues: { values: { string1: 'own string1', }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { string1: 'own string1', }, }, defaultsTrue: { mode: 'mode1', values: { string1: 'own string1', }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { string1: 'own string1', }, }, defaultsTrue: { mode: 'mode1', values: { string1: 'own string1', }, }, }, }, }, // Remember it is correct that default strings get returned here even when returnDefaults // is set to false because if they would not, there would be no way to know which value // got added and which one not. { description: 'complex type "collection" with "multipleValues: false" and with displayOptions "show" (no-match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'collection', default: {}, options: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode2'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], nodeValues: { values: { string1: 'own string1', }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { // TODO: Write some code which cleans up data like that values: {}, }, defaultsTrue: { mode: 'mode1', values: {}, }, }, noneDisplayedTrue: { defaultsFalse: { values: { string1: 'own string1', }, }, defaultsTrue: { mode: 'mode1', values: { string1: 'own string1', }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false" preserves explicitly added options with all default values (GitHub case)', input: { nodePropertiesArray: [ { name: 'additionalParameters', displayName: 'Additional Parameters', type: 'fixedCollection', default: {}, options: [ { name: 'author', displayName: 'Author', values: [ { name: 'name', displayName: 'Name', type: 'string', default: '', }, { name: 'email', displayName: 'Email', type: 'string', default: '', }, ], }, { name: 'branch', displayName: 'Branch', values: [ { name: 'branch', displayName: 'Branch', type: 'string', default: '', }, ], }, ], }, ], nodeValues: { additionalParameters: { author: { name: '', email: '', }, branch: { branch: '', }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { additionalParameters: { author: {}, branch: {}, }, }, defaultsTrue: { additionalParameters: { author: { name: '', email: '', }, branch: { branch: '', }, }, }, }, noneDisplayedTrue: { defaultsFalse: { additionalParameters: { author: {}, branch: {}, }, }, defaultsTrue: { additionalParameters: { author: { name: '', email: '', }, branch: { branch: '', }, }, }, }, }, }, // Remember it is correct that default strings get returned here even when returnDefaults // is set to false because if they would not, there would be no way to know which value // got added and which one not. { description: 'complex type "collection" with "multipleValues: true" and with displayOptions "show" (match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'collection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode1'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], nodeValues: { values: [ { string1: 'own string1', }, ], }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: [ { string1: 'own string1', }, ], }, defaultsTrue: { mode: 'mode1', values: [ { string1: 'own string1', }, ], }, }, noneDisplayedTrue: { defaultsFalse: { values: [ { string1: 'own string1', }, ], }, defaultsTrue: { mode: 'mode1', values: [ { string1: 'own string1', }, ], }, }, }, }, // Remember it is correct that default strings get returned here even when returnDefaults // is set to false because if they would not, there would be no way to know which value // got added and which one not. { description: 'complex type "collection" with "multipleValues: true" and with displayOptions "show" (no-match) on option which references root-value. One value set.', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { name: 'values', displayName: 'Values', type: 'collection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'string1', displayName: 'string1', type: 'string', displayOptions: { show: { '/mode': ['mode2'], }, }, default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], nodeValues: { values: [ { string1: 'own string1', number1: 0, }, ], }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: [ { string1: 'own string1', number1: 0, }, ], }, defaultsTrue: { mode: 'mode1', values: [ { string1: 'own string1', number1: 0, }, ], }, }, noneDisplayedTrue: { defaultsFalse: { values: [ { string1: 'own string1', number1: 0, }, ], }, defaultsTrue: { mode: 'mode1', values: [ { string1: 'own string1', number1: 0, }, ], }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: false" and with displayOptions "show" (no-match) on option. One value set also the not displayed one.', input: { nodePropertiesArray: [ { name: 'values', displayName: 'Values', type: 'fixedCollection', default: {}, options: [ { name: 'number', displayName: 'Number', values: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { displayName: 'string1', name: 'string1', type: 'string', displayOptions: { show: { mode: ['mode1'], }, }, default: 'default string1', }, { displayName: 'number1', name: 'number1', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number: { mode: 'mode2', string1: 'default string1', number1: 1, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number: { mode: 'mode2', number1: 1, }, }, }, defaultsTrue: { values: { number: { mode: 'mode2', number1: 1, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number: { mode: 'mode2', number1: 1, }, }, }, defaultsTrue: { values: { number: { mode: 'mode2', string1: 'default string1', number1: 1, }, }, }, }, }, }, { description: 'complex type "collection" with "multipleValues: true". One none-default value set.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', typeOptions: { multipleValues: true, }, default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: { collection1: [ { string1: 'value1', }, ], }, }, output: { noneDisplayedFalse: { defaultsFalse: { collection1: [ { string1: 'value1', }, ], }, defaultsTrue: { collection1: [ { string1: 'value1', }, ], }, }, noneDisplayedTrue: { defaultsFalse: { collection1: [ { string1: 'value1', }, ], }, defaultsTrue: { collection1: [ { string1: 'value1', }, ], }, }, }, }, // Remember it is correct that default strings get returned here even when returnDefaults // is set to false because if they would not, there would be no way to know which value // got added and which one not. { description: 'complex type "collection" with "multipleValues: true". One default value set.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', typeOptions: { multipleValues: true, }, default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: { collection1: [ { string1: 'default string1', }, ], }, }, output: { noneDisplayedFalse: { defaultsFalse: { collection1: [ { string1: 'default string1', }, ], }, defaultsTrue: { collection1: [ { string1: 'default string1', }, ], }, }, noneDisplayedTrue: { defaultsFalse: { collection1: [ { string1: 'default string1', }, ], }, defaultsTrue: { collection1: [ { string1: 'default string1', }, ], }, }, }, }, { description: 'complex type "collection" with "multipleValues: false". One none-default value set.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: { collection1: { string1: 'own string1', }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { collection1: { string1: 'own string1', }, }, defaultsTrue: { collection1: { string1: 'own string1', }, }, }, noneDisplayedTrue: { defaultsFalse: { collection1: { string1: 'own string1', }, }, defaultsTrue: { collection1: { string1: 'own string1', }, }, }, }, }, { description: 'complex type "collection" with "multipleValues: false". One default value set.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: { collection1: { string1: 'default string1', }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { collection1: { string1: 'default string1', }, }, defaultsTrue: { collection1: { string1: 'default string1', }, }, }, noneDisplayedTrue: { defaultsFalse: { collection1: { string1: 'default string1', }, }, defaultsTrue: { collection1: { string1: 'default string1', }, }, }, }, }, { description: 'complex type "collection" with "multipleValues: false". Only outer value set.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: { collection1: {}, }, }, output: { noneDisplayedFalse: { defaultsFalse: { collection1: {}, }, defaultsTrue: { collection1: {}, }, }, noneDisplayedTrue: { defaultsFalse: { collection1: {}, }, defaultsTrue: { collection1: {}, }, }, }, }, { description: 'complex type "collection" with "multipleValues: false". No value set at all.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', default: {}, options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { collection1: {}, }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { collection1: {}, }, }, }, }, { description: 'complex type "collection" with "multipleValues: true". No value set at all.', input: { nodePropertiesArray: [ { displayName: 'collection1', name: 'collection1', type: 'collection', typeOptions: { multipleValues: true, }, default: [], options: [ { displayName: 'string1', name: 'string1', type: 'string', default: 'default string1', }, { displayName: 'string2', name: 'string2', type: 'string', default: 'default string2', }, ], }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { collection1: [], }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { collection1: [], }, }, }, }, { description: 'two identically named properties of which only one gets displayed with different options. No value set at all.', input: { nodePropertiesArray: [ { displayName: 'mainOption', name: 'mainOption', type: 'options', options: [ { name: 'option1', value: 'option1', }, { name: 'option2', value: 'option2', }, ], default: 'option1', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option1'], }, }, options: [ { name: 'option1a', value: 'option1a', }, { name: 'option1b', value: 'option1b', }, ], default: 'option1a', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option2'], }, }, options: [ { name: 'option2a', value: 'option2a', }, { name: 'option2b', value: 'option2b', }, ], default: 'option2a', }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', }, }, }, }, { description: 'One property which is dependency on two identically named properties of which only one gets displayed with different options. No value set at all.', input: { nodePropertiesArray: [ { displayName: 'mainOption', name: 'mainOption', type: 'options', options: [ { name: 'option1', value: 'option1', }, { name: 'option2', value: 'option2', }, ], default: 'option1', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option1'], }, }, options: [ { name: 'option1a', value: 'option1a', }, { name: 'option1b', value: 'option1b', }, ], default: 'option1a', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option2'], }, }, options: [ { name: 'option2a', value: 'option2a', }, { name: 'option2b', value: 'option2b', }, ], default: 'option2a', }, { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value1', required: true, displayOptions: { show: { mainOption: ['option1'], subOption: ['option1a'], }, }, }, { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value2', required: true, displayOptions: { show: { mainOption: ['option2'], subOption: ['option2a'], }, }, }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', dependentParameter: 'value1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', dependentParameter: 'value1', }, }, }, }, { description: 'One property which is dependency on two identically named properties of which only one gets displayed with different options. No value set at all. Order reversed', input: { nodePropertiesArray: [ { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value2', required: true, displayOptions: { show: { mainOption: ['option2'], subOption: ['option2a'], }, }, }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option2'], }, }, options: [ { name: 'option2a', value: 'option2a', }, { name: 'option2b', value: 'option2b', }, ], default: 'option2a', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option1'], }, }, options: [ { name: 'option1a', value: 'option1a', }, { name: 'option1b', value: 'option1b', }, ], default: 'option1a', }, { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value1', required: true, displayOptions: { show: { mainOption: ['option1'], subOption: ['option1a'], }, }, }, { displayName: 'mainOption', name: 'mainOption', type: 'options', options: [ { name: 'option1', value: 'option1', }, { name: 'option2', value: 'option2', }, ], default: 'option1', }, ], nodeValues: {}, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', dependentParameter: 'value1', }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { mainOption: 'option1', subOption: 'option1a', dependentParameter: 'value1', }, }, }, }, { description: 'One property which is dependency on two identically named properties of which only one gets displayed with different options. No value set at all.', input: { nodePropertiesArray: [ { displayName: 'mainOption', name: 'mainOption', type: 'options', options: [ { name: 'option1', value: 'option1', }, { name: 'option2', value: 'option2', }, ], default: 'option1', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option1'], }, }, options: [ { name: 'option1a', value: 'option1a', }, { name: 'option1b', value: 'option1b', }, ], default: 'option1a', }, { displayName: 'subOption', name: 'subOption', type: 'options', displayOptions: { show: { mainOption: ['option2'], }, }, options: [ { name: 'option2a', value: 'option2a', }, { name: 'option2b', value: 'option2b', }, ], default: 'option2a', }, { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value1', required: true, displayOptions: { show: { mainOption: ['option1'], subOption: ['option1a'], }, }, }, { displayName: 'dependentParameter', name: 'dependentParameter', type: 'string', default: 'value2', required: true, displayOptions: { show: { mainOption: ['option2'], subOption: ['option2a'], }, }, }, ], nodeValues: { mainOption: 'option2', }, }, output: { noneDisplayedFalse: { defaultsFalse: { mainOption: 'option2', }, defaultsTrue: { mainOption: 'option2', subOption: 'option2a', dependentParameter: 'value2', }, }, noneDisplayedTrue: { defaultsFalse: { mainOption: 'option2', }, defaultsTrue: { mainOption: 'option2', subOption: 'option2a', dependentParameter: 'value2', }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". Which contains complex type "fixedCollection" with "multipleValues: true". One value set.', input: { nodePropertiesArray: [ { displayName: 'Values1', name: 'values1', type: 'fixedCollection', typeOptions: { multipleValues: true, }, description: 'The value to set.', default: {}, options: [ { displayName: 'Options1', name: 'options1', values: [ { displayName: 'Values2', name: 'values2', type: 'fixedCollection', typeOptions: { multipleValues: true, }, description: 'The value to set.', default: {}, options: [ { displayName: 'Options2', name: 'options2', values: [ { name: 'string1', displayName: 'string1', type: 'string', default: 'default string1', }, { name: 'number1', displayName: 'number1', type: 'number', default: 0, }, ], }, ], }, ], }, ], }, ], nodeValues: { values1: { options1: [ { values2: { options2: [ { number1: 1, }, ], }, }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values1: { options1: [ { values2: { options2: [ { number1: 1, }, ], }, }, ], }, }, defaultsTrue: { values1: { options1: [ { values2: { options2: [ { string1: 'default string1', number1: 1, }, ], }, }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values1: { options1: [ { values2: { options2: [ { number1: 1, }, ], }, }, ], }, }, defaultsTrue: { values1: { options1: [ { values2: { options2: [ { string1: 'default string1', number1: 1, }, ], }, }, ], }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameters which get displayed on a parameter with a default expression with relative parameter references.', input: { nodePropertiesArray: [ { displayName: 'Values1', name: 'values1', type: 'fixedCollection', typeOptions: { multipleValues: true, }, description: 'The value to set.', default: {}, options: [ { displayName: 'Options1', name: 'options1', values: [ { displayName: 'Key', name: 'key', type: 'string', default: '', }, { displayName: 'Type', name: 'type', type: 'hidden', default: '={{$parameter["&key"].split("|")[1]}}', }, { displayName: 'Title Value', name: 'titleValue', displayOptions: { show: { type: ['title'], }, }, type: 'string', default: 'defaultTitle', }, { displayName: 'Title Number', name: 'numberValue', displayOptions: { show: { type: ['number'], }, }, type: 'number', default: 1, }, ], }, ], }, ], nodeValues: { values1: { options1: [ { key: 'asdf|title', titleValue: 'different', }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values1: { options1: [ { key: 'asdf|title', titleValue: 'different', }, ], }, }, defaultsTrue: { values1: { options1: [ { key: 'asdf|title', type: '={{$parameter["&key"].split("|")[1]}}', // This is not great that it displays this theoretically hidden parameter // but because we can not resolve the values for now numberValue: 1, titleValue: 'different', }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values1: { options1: [ { key: 'asdf|title', titleValue: 'different', }, ], }, }, defaultsTrue: { values1: { options1: [ { key: 'asdf|title', type: '={{$parameter["&key"].split("|")[1]}}', titleValue: 'different', numberValue: 1, }, ], }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameter of type "multiOptions" and has so an array default value', input: { nodePropertiesArray: [ { name: 'values', displayName: 'Values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'propertyValues', displayName: 'Property', values: [ { displayName: 'Options', name: 'multiSelectValue', type: 'multiOptions', options: [ { name: 'Value1', value: 'value1', }, { name: 'Value2', value: 'value2', }, ], default: [], }, ], }, ], }, ], nodeValues: { values: { propertyValues: [ { multiSelectValue: [], }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { propertyValues: [{}], }, }, defaultsTrue: { values: { propertyValues: [ { multiSelectValue: [], }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { propertyValues: [{}], }, }, defaultsTrue: { values: { propertyValues: [ { multiSelectValue: [], }, ], }, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameter of type "string" with "multipleValues: true" and a custom default value', input: { nodePropertiesArray: [ { name: 'values', displayName: 'Values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'propertyValues', displayName: 'Property', values: [ { displayName: 'MultiString', name: 'multiString', type: 'string', typeOptions: { multipleValues: true, }, default: ['value1'], }, ], }, ], }, ], nodeValues: { values: { propertyValues: [ { multiString: ['value1'], }, ], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { propertyValues: [{}], }, }, defaultsTrue: { values: { propertyValues: [ { multiString: ['value1'], }, ], }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { propertyValues: [{}], }, }, defaultsTrue: { values: { propertyValues: [ { multiString: ['value1'], }, ], }, }, }, }, }, { description: 'complex type "collection" which contains a "fixedCollection" with "multipleValues: false" that has all values set to the default values (by having it as an empty object) in combination with another value', input: { nodePropertiesArray: [ { name: 'mode', displayName: 'mode', type: 'string', default: 'mode1', }, { displayName: 'Options', name: 'options', placeholder: 'Add Option', type: 'collection', default: {}, options: [ { displayName: 'Sort', name: 'sort', type: 'fixedCollection', typeOptions: { multipleValues: false, }, default: {}, placeholder: 'Add Sort', options: [ { displayName: 'Sort', name: 'value', values: [ { displayName: 'Descending', name: 'descending', type: 'boolean', default: true, description: 'Sort by descending order', }, { displayName: 'Order By', name: 'ordering', type: 'options', default: 'date', options: [ { name: 'Date', value: 'date', }, { name: 'Name', value: 'name', }, ], }, ], }, ], }, ], }, ], nodeValues: { mode: 'changed', options: { sort: { value: {}, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { mode: 'changed', options: { sort: { value: {}, }, }, }, defaultsTrue: { mode: 'changed', options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { mode: 'changed', options: { sort: { value: {}, }, }, }, defaultsTrue: { mode: 'changed', options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, }, }, { description: 'complex type "collection" which contains a "fixedCollection" with "multipleValues: false" that has all values set to the default values (by having it as an empty object)', input: { nodePropertiesArray: [ { displayName: 'Options', name: 'options', placeholder: 'Add Option', type: 'collection', default: {}, options: [ { displayName: 'Sort', name: 'sort', type: 'fixedCollection', typeOptions: { multipleValues: false, }, default: {}, placeholder: 'Add Sort', options: [ { displayName: 'Sort', name: 'value', values: [ { displayName: 'Descending', name: 'descending', type: 'boolean', default: true, description: 'Sort by descending order', }, { displayName: 'Order By', name: 'ordering', type: 'options', default: 'date', options: [ { name: 'Date', value: 'date', }, { name: 'Name', value: 'name', }, ], }, ], }, ], }, ], }, ], nodeValues: { options: { sort: { value: {}, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { options: { sort: { value: {}, }, }, }, defaultsTrue: { options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { options: { sort: { value: {}, }, }, }, defaultsTrue: { options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, }, }, { description: 'complex type "collection" which contains a "fixedCollection" with "multipleValues: false" that has all values set to the default values (by having each value set)', input: { nodePropertiesArray: [ { displayName: 'Options', name: 'options', placeholder: 'Add Option', type: 'collection', default: {}, options: [ { displayName: 'Sort', name: 'sort', type: 'fixedCollection', typeOptions: { multipleValues: false, }, default: {}, options: [ { displayName: 'Sort', name: 'value', values: [ { displayName: 'Descending', name: 'descending', type: 'boolean', default: true, }, { displayName: 'Order By', name: 'ordering', type: 'options', default: 'date', options: [ { name: 'Date', value: 'date', }, { name: 'Name', value: 'name', }, ], }, ], }, ], }, ], }, ], nodeValues: { options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { options: { sort: { value: {}, }, }, }, defaultsTrue: { options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, noneDisplayedTrue: { defaultsFalse: { options: { sort: { value: {}, }, }, }, defaultsTrue: { options: { sort: { value: { descending: true, ordering: 'date', }, }, }, }, }, }, }, { description: 'nodeValues is null (for example when resolving expression fails)', input: { nodePropertiesArray: [ { displayName: 'Custom Properties', name: 'customPropertiesUi', placeholder: 'Add Custom Property', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'customPropertiesValues', displayName: 'Custom Property', values: [ { displayName: 'Property Name or ID', name: 'property', type: 'options', typeOptions: { loadOptionsMethod: 'getDealCustomProperties', }, default: '', description: 'Name of the property. Choose from the list, or specify an ID using an expression.', }, { displayName: 'Value', name: 'value', type: 'string', default: '', required: true, description: 'Value of the property', }, ], }, ], }, ], nodeValues: null, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: {}, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: {}, }, }, }, { description: 'fixedCollection with multipleValues: true - skip when propertyValues is not an object or is an array', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'option1', displayName: 'Option 1', values: [ { displayName: 'String', name: 'string1', type: 'string', default: 'default string', }, ], }, ], }, ], nodeValues: { // This simulates when propertyValues is incorrectly set as an array instead of an object values: [] as any, }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { values: {}, }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { values: {}, }, }, }, }, { description: 'fixedCollection with multipleValues: true - skip when propertyValues is a string', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'option1', displayName: 'Option 1', values: [ { displayName: 'String', name: 'string1', type: 'string', default: 'default string', }, ], }, ], }, ], nodeValues: { // This simulates when propertyValues is incorrectly set as a string values: 'invalid value' as any, }, }, output: { noneDisplayedFalse: { defaultsFalse: {}, defaultsTrue: { values: {}, }, }, noneDisplayedTrue: { defaultsFalse: {}, defaultsTrue: { values: {}, }, }, }, }, { description: 'complex type "fixedCollection" with "multipleValues: true". Skip unknown item names instead of throwing', input: { nodePropertiesArray: [ { displayName: 'Values', name: 'values', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, options: [ { name: 'number1', displayName: 'Number 1', values: [ { displayName: 'Number', name: 'number', type: 'number', default: 0, }, ], }, ], }, ], nodeValues: { values: { number1: [{ number: 42 }], and: [{ property: 'Status', filter: 'active' }], }, }, }, output: { noneDisplayedFalse: { defaultsFalse: { values: { number1: [{ number: 42 }], }, }, defaultsTrue: { values: { number1: [{ number: 42 }], }, }, }, noneDisplayedTrue: { defaultsFalse: { values: { number1: [{ number: 42 }], }, }, defaultsTrue: { values: { number1: [{ number: 42 }], }, }, }, }, }, ]; for (const testData of tests) { test(testData.description, () => { // returnDefaults: false | returnNoneDisplayed: false let result = getNodeParameters( testData.input.nodePropertiesArray, testData.input.nodeValues, false, false, null, null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsFalse); // returnDefaults: true | returnNoneDisplayed: false result = getNodeParameters( testData.input.nodePropertiesArray, testData.input.nodeValues, true, false, null, null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsTrue); // returnDefaults: false | returnNoneDisplayed: true result = getNodeParameters( testData.input.nodePropertiesArray, testData.input.nodeValues, false, true, null, null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsFalse); // returnDefaults: true | returnNoneDisplayed: true result = getNodeParameters( testData.input.nodePropertiesArray, testData.input.nodeValues, true, true, null, null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsTrue); }); } test('does not treat showOnDeployment as a parameter dependency', () => { const properties: INodeProperties[] = [ { displayName: 'Operation', name: 'operation', type: 'options', options: [{ name: 'Read', value: 'read' }], default: 'read', }, { displayName: 'Cloud notice', name: 'cloudNotice', type: 'notice', default: '', displayOptions: { show: { operation: ['read'] }, showOnDeployment: 'cloud', }, }, ]; expect(() => getNodeParameters(properties, {}, true, false, null, null)).not.toThrow(); }); }); describe('isSubNodeType', () => { const tests: Array<[boolean, Pick | null]> = [ [false, null], [false, { outputs: '={{random_expression}}' }], [false, { outputs: [] }], [false, { outputs: [NodeConnectionTypes.Main] }], [true, { outputs: [NodeConnectionTypes.AiAgent] }], [true, { outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiAgent] }], ]; test.each(tests)('should return %p for %o', (expected, nodeType) => { expect(isSubNodeType(nodeType)).toBe(expected); }); }); describe('getParameterIssues', () => { const tests: Array<{ description: string; input: { nodeProperties: INodeProperties; nodeValues: INodeParameters; path: string; node: INode; }; output: INodeIssues; }> = [ { description: 'Fixed collection::Should not return issues if minimum or maximum field count is not set', input: { nodeProperties: { displayName: 'Workflow Inputs', name: 'workflowInputs', placeholder: 'Add Field', type: 'fixedCollection', description: 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', typeOptions: { multipleValues: true, sortable: true, }, displayOptions: { show: { '@version': [ { _cnd: { gte: 1.1, }, }, ], inputSource: ['workflowInputs'], }, }, default: {}, options: [ { name: 'values', displayName: 'Values', values: [ { displayName: 'Name', name: 'name', type: 'string', default: '', placeholder: 'e.g. fieldName', description: 'Name of the field', noDataExpression: true, }, { displayName: 'Type', name: 'type', type: 'options', description: 'The field value type', options: [ { name: 'Allow Any Type', value: 'any', }, { name: 'String', value: 'string', }, { name: 'Number', value: 'number', }, { name: 'Boolean', value: 'boolean', }, { name: 'Array', value: 'array', }, { name: 'Object', value: 'object', }, ], default: 'string', noDataExpression: true, }, ], }, ], }, nodeValues: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, path: '', node: { parameters: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, type: 'n8n-nodes-base.executeWorkflowTrigger', typeVersion: 1.1, position: [-140, -20], id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', name: 'Test Node', } as INode, }, output: {}, }, { description: 'Fixed collection::Should not return issues if field count is within the specified range', input: { nodeProperties: { displayName: 'Workflow Inputs', name: 'workflowInputs', placeholder: 'Add Field', type: 'fixedCollection', description: 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', typeOptions: { multipleValues: true, sortable: true, minRequiredFields: 1, maxAllowedFields: 3, }, displayOptions: { show: { '@version': [ { _cnd: { gte: 1.1, }, }, ], inputSource: ['workflowInputs'], }, }, default: {}, options: [ { name: 'values', displayName: 'Values', values: [ { displayName: 'Name', name: 'name', type: 'string', default: '', placeholder: 'e.g. fieldName', description: 'Name of the field', noDataExpression: true, }, { displayName: 'Type', name: 'type', type: 'options', description: 'The field value type', options: [ { name: 'Allow Any Type', value: 'any', }, { name: 'String', value: 'string', }, { name: 'Number', value: 'number', }, { name: 'Boolean', value: 'boolean', }, { name: 'Array', value: 'array', }, { name: 'Object', value: 'object', }, ], default: 'string', noDataExpression: true, }, ], }, ], }, nodeValues: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: { values: [ { name: 'field1', type: 'string', }, { name: 'field2', type: 'string', }, ], }, inputOptions: {}, }, path: '', node: { parameters: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, type: 'n8n-nodes-base.executeWorkflowTrigger', typeVersion: 1.1, position: [-140, -20], id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', name: 'Test Node', } as INode, }, output: {}, }, { description: 'Fixed collection::Should return an issue if field count is lower than minimum specified', input: { nodeProperties: { displayName: 'Workflow Inputs', name: 'workflowInputs', placeholder: 'Add Field', type: 'fixedCollection', description: 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', typeOptions: { multipleValues: true, sortable: true, minRequiredFields: 1, }, displayOptions: { show: { '@version': [ { _cnd: { gte: 1.1, }, }, ], inputSource: ['workflowInputs'], }, }, default: {}, options: [ { name: 'values', displayName: 'Values', values: [ { displayName: 'Name', name: 'name', type: 'string', default: '', placeholder: 'e.g. fieldName', description: 'Name of the field', noDataExpression: true, }, { displayName: 'Type', name: 'type', type: 'options', description: 'The field value type', options: [ { name: 'Allow Any Type', value: 'any', }, { name: 'String', value: 'string', }, { name: 'Number', value: 'number', }, { name: 'Boolean', value: 'boolean', }, { name: 'Array', value: 'array', }, { name: 'Object', value: 'object', }, ], default: 'string', noDataExpression: true, }, ], }, ], }, nodeValues: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, path: '', node: { parameters: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, type: 'n8n-nodes-base.executeWorkflowTrigger', typeVersion: 1.1, position: [-140, -20], id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', name: 'Test Node', } as INode, }, output: { parameters: { workflowInputs: ['At least 1 field is required.'], }, }, }, { description: 'Fixed collection::Should return an issue if field count is higher than maximum specified', input: { nodeProperties: { displayName: 'Workflow Inputs', name: 'workflowInputs', placeholder: 'Add Field', type: 'fixedCollection', description: 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', typeOptions: { multipleValues: true, sortable: true, maxAllowedFields: 1, }, displayOptions: { show: { '@version': [ { _cnd: { gte: 1.1, }, }, ], inputSource: ['workflowInputs'], }, }, default: {}, options: [ { name: 'values', displayName: 'Values', values: [ { displayName: 'Name', name: 'name', type: 'string', default: '', placeholder: 'e.g. fieldName', description: 'Name of the field', noDataExpression: true, }, { displayName: 'Type', name: 'type', type: 'options', description: 'The field value type', options: [ { name: 'Allow Any Type', value: 'any', }, { name: 'String', value: 'string', }, { name: 'Number', value: 'number', }, { name: 'Boolean', value: 'boolean', }, { name: 'Array', value: 'array', }, { name: 'Object', value: 'object', }, ], default: 'string', noDataExpression: true, }, ], }, ], }, nodeValues: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: { values: [ { name: 'field1', type: 'string', }, { name: 'field2', type: 'string', }, ], }, inputOptions: {}, }, path: '', node: { parameters: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, type: 'n8n-nodes-base.executeWorkflowTrigger', typeVersion: 1.1, position: [-140, -20], id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', name: 'Test Node', } as INode, }, output: { parameters: { workflowInputs: ['At most 1 field is allowed.'], }, }, }, { description: 'Fixed collection::Should not return issues if the collection is hidden', input: { nodeProperties: { displayName: 'Workflow Inputs', name: 'workflowInputs', placeholder: 'Add Field', type: 'fixedCollection', description: 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', typeOptions: { multipleValues: true, sortable: true, maxAllowedFields: 1, }, displayOptions: { show: { '@version': [ { _cnd: { gte: 1.1, }, }, ], inputSource: ['workflowInputs'], }, }, default: {}, options: [ { name: 'values', displayName: 'Values', values: [ { displayName: 'Name', name: 'name', type: 'string', default: '', placeholder: 'e.g. fieldName', description: 'Name of the field', noDataExpression: true, }, { displayName: 'Type', name: 'type', type: 'options', description: 'The field value type', options: [ { name: 'Allow Any Type', value: 'any', }, { name: 'String', value: 'string', }, { name: 'Number', value: 'number', }, { name: 'Boolean', value: 'boolean', }, { name: 'Array', value: 'array', }, { name: 'Object', value: 'object', }, ], default: 'string', noDataExpression: true, }, ], }, ], }, nodeValues: { events: 'worklfow_call', inputSource: 'somethingElse', workflowInputs: { values: [ { name: 'field1', type: 'string', }, { name: 'field2', type: 'string', }, ], }, inputOptions: {}, }, path: '', node: { parameters: { events: 'worklfow_call', inputSource: 'workflowInputs', workflowInputs: {}, inputOptions: {}, }, type: 'n8n-nodes-base.executeWorkflowTrigger', typeVersion: 1.1, position: [-140, -20], id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', name: 'Test Node', } as INode, }, output: {}, }, ]; for (const testData of tests) { test(testData.description, () => { const result = getParameterIssues( testData.input.nodeProperties, testData.input.nodeValues, testData.input.path, testData.input.node, null, ); expect(result).toEqual(testData.output); }); } }); describe('getParameterIssues, required parameters validation', () => { const testNode: INode = { id: '12345', name: 'Test Node', typeVersion: 1, type: 'n8n-nodes-base.testNode', position: [1, 1], parameters: {}, }; const testNodeType: INodeTypeDescription = { name: 'Test Node', version: 0, defaults: {}, inputs: [], outputs: [], properties: [], displayName: '', group: [], description: '', }; it('Should validate required dateTime parameters if empty string', () => { const nodeProperties: INodeProperties = { displayName: 'Date Time', name: 'testDateTime', type: 'dateTime', default: '', required: true, }; const nodeValues: INodeParameters = { testDateTime: '', }; const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, null); expect(result).toEqual({ parameters: { testDateTime: ['Parameter "Date Time" is required.'], }, }); }); it('Should validate required dateTime parameters if empty undefined', () => { const nodeProperties: INodeProperties = { displayName: 'Date Time', name: 'testDateTime', type: 'dateTime', default: '', required: true, }; const nodeValues: INodeParameters = { testDateTime: undefined, }; const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, testNodeType); expect(result).toEqual({ parameters: { testDateTime: ['Parameter "Date Time" is required.'], }, }); }); it('Should treat agentSelector like a resource locator when required and empty', () => { const nodeProperties: INodeProperties = { displayName: 'Agent', name: 'agentId', type: 'agentSelector', default: { mode: 'list', value: '' }, required: true, modes: [ { displayName: 'From List', name: 'list', type: 'list' }, { displayName: 'By ID', name: 'id', type: 'string' }, ], }; const nodeValues: INodeParameters = { agentId: { __rl: true, mode: 'list', value: '' }, }; const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, null); expect(result).toEqual({ parameters: { agentId: ['Parameter "Agent" is required.'], }, }); }); it('Should not report an issue for a populated agentSelector', () => { const nodeProperties: INodeProperties = { displayName: 'Agent', name: 'agentId', type: 'agentSelector', default: { mode: 'list', value: '' }, required: true, modes: [ { displayName: 'From List', name: 'list', type: 'list' }, { displayName: 'By ID', name: 'id', type: 'string' }, ], }; const nodeValues: INodeParameters = { agentId: { __rl: true, mode: 'list', value: 'agent-1' }, }; const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, null); expect(result).toEqual({}); }); }); describe('nodeIssuesToString', () => { it('returns an empty list when no issues are set', () => { expect(nodeIssuesToString({})).toEqual([]); }); it('flattens every issue type in order (execution, parameters, credentials, input, typeUnknown)', () => { const issues: INodeIssues = { execution: true, parameters: { url: ['Parameter "URL" is required.', 'Parameter "URL" must be a string.'], method: ['Parameter "Method" is required.'], }, credentials: { api: ['Credentials for "api" are not set.'] }, input: { main: ['No node connected to required input "main"'] }, typeUnknown: true, }; const node: INode = { id: '1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, position: [0, 0], parameters: {}, }; expect(nodeIssuesToString(issues, node)).toEqual([ 'Execution Error.', 'Parameter "URL" is required.', 'Parameter "URL" must be a string.', 'Parameter "Method" is required.', 'Credentials for "api" are not set.', 'No node connected to required input "main"', 'Node Type "n8n-nodes-base.httpRequest" is not known.', ]); }); it('falls back to a generic typeUnknown message when no node is passed', () => { expect(nodeIssuesToString({ typeUnknown: true })).toEqual(['Node Type is not known.']); }); }); describe('isTriggerNode', () => { const tests: Array<{ description: string; input: INodeTypeDescription; expected: boolean; }> = [ { description: 'Should return true for node with trigger in group', input: { name: 'TriggerNode', displayName: 'Trigger Node', group: ['trigger'], description: 'Trigger node description', version: 1, defaults: {}, inputs: [], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: true, }, { description: 'Should return true for node with multiple groups including trigger', input: { name: 'MultiGroupTriggerNode', displayName: 'Multi-Group Trigger Node', group: ['trigger', 'input'], description: 'Multi-group trigger node description', version: 1, defaults: {}, inputs: [], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: true, }, { description: 'Should return false for node without trigger in group', input: { name: 'RegularNode', displayName: 'Regular Node', group: ['input'], description: 'Regular node description', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: false, }, { description: 'Should return false for node with empty group array', input: { name: 'EmptyGroupNode', displayName: 'Empty Group Node', group: [], description: 'Empty group node description', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: false, }, { description: 'Should return false when trigger is called Trigger, but does not have a trigger group', input: { name: 'AlmostTriggerNode', displayName: 'Almost Trigger Node', group: ['transform'], description: 'Almost trigger node description', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: false, }, ]; for (const testData of tests) { test(testData.description, () => { const result = isTriggerNode(testData.input); expect(result).toEqual(testData.expected); }); } }); describe('getNodeOutputs', () => { const workflowMock = { expression: { getSimpleParameterValue: vi.fn().mockReturnValue([NodeConnectionTypes.Main]), }, } as unknown as Workflow; test('Should return empty array when nodeTypeData is null', () => { const node: INode = { id: 'testNodeId', name: 'TestNode', position: [0, 0], type: 'n8n-nodes-base.TestNode', typeVersion: 1, parameters: {}, }; const result = getNodeOutputs(workflowMock, node, null as unknown as INodeTypeDescription); expect(result).toEqual([]); }); test('Should return empty array when nodeTypeData is undefined', () => { const node: INode = { id: 'testNodeId', name: 'TestNode', position: [0, 0], type: 'n8n-nodes-base.TestNode', typeVersion: 1, parameters: {}, }; const result = getNodeOutputs( workflowMock, node, undefined as unknown as INodeTypeDescription, ); expect(result).toEqual([]); }); test('Should return outputs array when nodeTypeData is valid', () => { const node: INode = { id: 'testNodeId', name: 'TestNode', position: [0, 0], type: 'n8n-nodes-base.TestNode', typeVersion: 1, parameters: {}, }; const nodeTypeData: INodeTypeDescription = { name: 'TestNode', displayName: 'Test Node', group: ['transform'], description: 'Test node description', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiAgent], properties: [], }; const result = getNodeOutputs(workflowMock, node, nodeTypeData); expect(result).toEqual([NodeConnectionTypes.Main, NodeConnectionTypes.AiAgent]); }); }); describe('isExecutable', () => { const workflowMock = { expression: { getSimpleParameterValue: vi.fn().mockReturnValue([NodeConnectionTypes.Main]), }, } as unknown as Workflow; const tests: Array<{ description: string; node: INode; nodeTypeData: INodeTypeDescription; expected: boolean; mockReturnValue?: NodeConnectionType[]; }> = [ { description: 'Should return true for trigger node', node: { id: 'triggerNodeId', name: 'TriggerNode', position: [0, 0], type: 'n8n-nodes-base.TriggerNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'TriggerNode', displayName: 'Trigger Node', group: ['trigger'], description: 'Trigger node description', version: 1, defaults: {}, inputs: [], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: true, }, { description: 'Should return true for node with Main output', node: { id: 'mainOutputNodeId', name: 'MainOutputNode', position: [0, 0], type: 'n8n-nodes-base.MainOutputNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'MainOutputNode', displayName: 'Main Output Node', group: ['transform'], description: 'Node with Main output', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main], properties: [], }, expected: true, }, { description: 'Should return false for node without Main output and not a trigger', node: { id: 'nonExecutableNodeId', name: 'NonExecutableNode', position: [0, 0], type: 'n8n-nodes-base.NonExecutableNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'NonExecutableNode', displayName: 'Non-Executable Node', group: ['output'], description: 'Node without Main output and not a trigger', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.AiAgent], properties: [], }, expected: false, }, { description: 'Should return true for node with mixed outputs including Main', node: { id: 'mixedOutputNodeId', name: 'MixedOutputNode', position: [0, 0], type: 'n8n-nodes-base.MixedOutputNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'MixedOutputNode', displayName: 'Mixed Output Node', group: ['transform'], description: 'Node with multiple output types including Main', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiAgent], properties: [], }, expected: true, }, { description: 'Should return true for node with only AiTool output and not a trigger', node: { id: 'aiToolOutputNodeId', name: 'AiToolOutputNode', position: [0, 0], type: 'n8n-nodes-base.AiToolOutputNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'AiToolOutputNode', displayName: 'AI Tool Output Node', group: ['output'], description: 'Node with only AiTool output and not a trigger', version: 1, defaults: {}, inputs: [], outputs: [NodeConnectionTypes.AiTool], // Only AiTool output, no Main properties: [], }, expected: true, }, { description: 'Should return true for node with dynamic outputs set to AiTool only', node: { id: 'dynamicAiToolNodeId', name: 'DynamicAiToolNode', position: [0, 0], type: 'n8n-nodes-base.DynamicAiToolNode', typeVersion: 1, parameters: {}, }, nodeTypeData: { name: 'DynamicAiToolNode', displayName: 'Dynamic AiTool Node', group: ['output'], description: 'Node with dynamic outputs that resolve to only AiTool', version: 1, defaults: {}, inputs: [NodeConnectionTypes.Main], outputs: '={{["ai_tool"]}}', // Dynamic expression that resolves to AiTool only properties: [], }, expected: true, mockReturnValue: [NodeConnectionTypes.AiTool], }, ]; for (const testData of tests) { test(testData.description, () => { // If this test has a custom mock return value, configure it if (testData.mockReturnValue) { // eslint-disable-next-line @typescript-eslint/unbound-method vi.mocked(workflowMock.expression.getSimpleParameterValue).mockReturnValueOnce( testData.mockReturnValue, ); } const result = isExecutable(workflowMock, testData.node, testData.nodeTypeData); expect(result).toEqual(testData.expected); }); } test('Should return false when nodeTypeData is null', () => { const node: INode = { id: 'testNodeId', name: 'TestNode', position: [0, 0], type: 'n8n-nodes-base.TestNode', typeVersion: 1, parameters: {}, }; const result = isExecutable(workflowMock, node, null as unknown as INodeTypeDescription); expect(result).toBe(false); }); test('Should return false when nodeTypeData is undefined', () => { const node: INode = { id: 'testNodeId', name: 'TestNode', position: [0, 0], type: 'n8n-nodes-base.TestNode', typeVersion: 1, parameters: {}, }; const result = isExecutable(workflowMock, node, undefined as unknown as INodeTypeDescription); expect(result).toBe(false); }); }); describe('displayParameter', () => { const testNode: INode = { id: '12345', name: 'Test Node', typeVersion: 1, type: 'n8n-nodes-base.testNode', position: [1, 1], parameters: {}, }; const testNodeType: INodeTypeDescription = { name: 'Test Node', version: 0, defaults: {}, inputs: [], outputs: [], properties: [], displayName: '', group: [], description: '', }; const defaultTestInput = { nodeValues: {}, parameter: { displayName: 'Test Parameter', name: 'testParameter', type: 'string', default: '', } as INodeProperties, node: testNode, nodeTypeDescription: testNodeType, nodeValuesRoot: undefined as undefined | INodeParameters, displayKey: 'displayOptions' as 'displayOptions' | 'disabledOptions', }; const tests: Array<[string, typeof defaultTestInput, boolean]> = [ ['Should return true if no displayOptions are defined', { ...defaultTestInput }, true], [ 'Should return true if displayOptions.show conditions are met', { ...defaultTestInput, nodeValues: { condition: 'value1' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { condition: ['value1'], }, }, }, }, true, ], [ 'Should return false if displayOptions.show conditions are not met', { ...defaultTestInput, nodeValues: { condition: 'value2' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { condition: ['value1'], }, }, }, }, false, ], [ 'Should return false if displayOptions.hide conditions are met', { ...defaultTestInput, nodeValues: { condition: 'value1' }, parameter: { ...defaultTestInput.parameter, displayOptions: { hide: { condition: ['value1'], }, }, }, }, false, ], [ 'Should return true if displayOptions.hide conditions are not met', { ...defaultTestInput, nodeValues: { condition: 'value2' }, parameter: { ...defaultTestInput.parameter, displayOptions: { hide: { condition: ['value1'], }, }, }, }, true, ], [ 'Should return true if displayOptions.show and hide conditions are both met', { ...defaultTestInput, nodeValues: { condition: 'value1' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { condition: ['value1'], }, hide: { condition: ['value1'], }, }, }, }, false, // Hide takes precedence over show ], [ 'Should return true if displayOptions.show conditions are met with multiple values', { ...defaultTestInput, nodeValues: { condition: 'value2' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { condition: ['value1', 'value2'], }, }, }, }, true, ], [ 'Should return false if displayOptions.hide conditions are met with multiple values', { ...defaultTestInput, nodeValues: { condition: 'value2' }, parameter: { ...defaultTestInput.parameter, displayOptions: { hide: { condition: ['value1', 'value2'], }, }, }, }, false, ], [ 'Should return true if @tool is true in nodeTypeDescription of tool', { ...defaultTestInput, nodeTypeDescription: { ...testNodeType, name: testNodeType.name + 'Tool', }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@tool': [true], }, }, }, }, true, ], [ 'Should return false if @tool is true in nodeTypeDescription of non-tool', { ...defaultTestInput, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@tool': [true], }, }, }, }, false, ], [ 'Should return true if @version condition is met', { ...defaultTestInput, node: { ...testNode, typeVersion: 2, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@version': [ { _cnd: { gte: 2, }, }, ], }, }, }, }, true, ], [ 'Should return false if @version condition is not met', { ...defaultTestInput, node: { ...testNode, typeVersion: 1, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@version': [ { _cnd: { gte: 2, }, }, ], }, }, }, }, false, ], [ 'Should return true if @tool and @version conditions are both met', { ...defaultTestInput, node: { ...testNode, typeVersion: 2, }, nodeTypeDescription: { ...testNodeType, name: testNodeType.name + 'Tool', }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@tool': [true], '@version': [ { _cnd: { gte: 2, }, }, ], }, }, }, }, true, ], [ 'Should return false if @tool is true but @version condition is not met', { ...defaultTestInput, node: { ...testNode, typeVersion: 1, }, nodeTypeDescription: { ...testNodeType, name: testNodeType.name + 'Tool', }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@tool': [true], '@version': [ { _cnd: { gte: 2, }, }, ], }, }, }, }, false, ], [ 'Should return true if no disabledOptions are defined', { ...defaultTestInput, displayKey: 'disabledOptions', }, true, ], [ 'Should return false if disabledOptions.hide conditions are met', { ...defaultTestInput, nodeValues: { condition: 'value1' }, parameter: { ...defaultTestInput.parameter, disabledOptions: { hide: { condition: ['value1'], }, }, }, displayKey: 'disabledOptions', }, false, ], [ 'Should return true if disabledOptions.hide conditions are not met', { ...defaultTestInput, nodeValues: { condition: 'value2' }, parameter: { ...defaultTestInput.parameter, disabledOptions: { hide: { condition: ['value1'], }, }, }, displayKey: 'disabledOptions', }, true, ], [ 'Should return true if nodeValuesRoot contains a matching value for displayOptions.show', { ...defaultTestInput, nodeValues: {}, nodeValuesRoot: { condition: 'value1' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '/condition': ['value1'], }, }, }, }, true, ], [ 'Should return false if nodeValuesRoot does not contain a matching value for displayOptions.show', { ...defaultTestInput, nodeValues: { condition: 'value1' }, nodeValuesRoot: { anotherKey: 'value1' }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '/condition': ['value1'], }, }, }, }, false, ], [ 'Should return true if @feature is enabled (simple string array)', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.4, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': ['useFeatureA'], }, }, }, }, true, ], [ 'Should return false if @feature is not enabled (simple string array)', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.3, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': ['useFeatureA'], }, }, }, }, false, ], [ 'Should return true if @feature condition eq is met', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.4, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': [{ _cnd: { eq: 'useFeatureA' } }], }, }, }, }, true, ], [ 'Should return false if @feature condition eq is not met', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.3, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': [{ _cnd: { eq: 'useFeatureA' } }], }, }, }, }, false, ], [ 'Should return true if @feature condition not is met (feature disabled)', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.3, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': [{ _cnd: { not: 'useFeatureA' } }], }, }, }, }, true, ], [ 'Should return false if @feature condition not is not met (feature enabled)', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.4, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': [{ _cnd: { not: 'useFeatureA' } }], }, }, }, }, false, ], [ 'Should return true if @feature with multiple conditions (OR logic)', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.4, }, nodeTypeDescription: { ...testNodeType, features: { useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] }, useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] }, }, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': [{ _cnd: { eq: 'useFeatureA' } }, { _cnd: { eq: 'useFeatureB' } }], }, }, }, }, true, ], [ 'Should return false if @feature is checked but node has no features', { ...defaultTestInput, node: { ...testNode, typeVersion: 2.4, }, nodeTypeDescription: { ...testNodeType, features: undefined, }, parameter: { ...defaultTestInput.parameter, displayOptions: { show: { '@feature': ['useFeatureA'], }, }, }, }, false, ], ]; for (const [description, input, expected] of tests) { test(description, () => { const result = displayParameter( input.nodeValues, input.parameter, input.node, input.nodeTypeDescription, input.nodeValuesRoot, input.displayKey, ); expect(result).toEqual(expected); }); } describe('_cnd string operators with undefined property value', () => { const node: INode = { ...testNode }; const nodeTypeDescription: INodeTypeDescription = { ...testNodeType }; const baseParam: INodeProperties = { displayName: 'Test', name: 'test', type: 'string', default: '', }; test('includes returns false when property is undefined', () => { const param: INodeProperties = { ...baseParam, displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } }, }; expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false); }); test('startsWith returns false when property is undefined', () => { const param: INodeProperties = { ...baseParam, displayOptions: { show: { '/missingParam': [{ _cnd: { startsWith: 'foo' } }] } }, }; expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false); }); test('endsWith returns false when property is undefined', () => { const param: INodeProperties = { ...baseParam, displayOptions: { show: { '/missingParam': [{ _cnd: { endsWith: 'foo' } }] } }, }; expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false); }); test('regex returns false when property is undefined', () => { const param: INodeProperties = { ...baseParam, displayOptions: { show: { '/missingParam': [{ _cnd: { regex: 'foo' } }] } }, }; expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false); }); test('includes returns true when property matches', () => { const param: INodeProperties = { ...baseParam, displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } }, }; expect(displayParameter({ missingParam: 'foobar' }, param, node, nodeTypeDescription)).toBe( true, ); }); }); }); describe('makeDescription', () => { let mockNodeTypeDescription: INodeTypeDescription; beforeEach(() => { // Arrange a basic mock node type description mockNodeTypeDescription = { displayName: 'Test Node', name: 'testNode', icon: 'fa:test', group: ['transform'], version: 1, description: 'This is a test node', defaults: { name: 'Test Node', }, inputs: ['main'], outputs: ['main'], properties: [], }; }); test('should return action-based description when action is available', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, ], default: 'create', }, ]; // Act const result = makeDescription(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create a new user in Test Node'); }); test('should return resource-operation-based description when action is not available', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', // No action property }, ], default: 'create', }, ]; // Act const result = makeDescription(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('create user in Test Node'); }); test('should return default description when resource or operation is missing', () => { // Arrange const nodeParameters: INodeParameters = { // No resource or operation }; // Act const result = makeDescription(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('This is a test node'); }); test('should handle case where nodeTypeOperation is not found', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ // No matching operation property ]; // Act const result = makeDescription(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('create user in Test Node'); }); test('should handle case where options are not a list of INodePropertyOptions', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, // Options are not INodePropertyOptions[] options: [ //@ts-expect-error {}, ], default: 'create', }, ]; // Act const result = makeDescription(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('create user in Test Node'); }); }); describe('getUpdatedToolDescription', () => { let mockNodeTypeDescription: INodeTypeDescription; beforeEach(() => { // Arrange a basic mock node type description mockNodeTypeDescription = { displayName: 'Test Node', name: 'testNode', icon: 'fa:test', group: ['transform'], version: 1, description: 'This is a test node', defaults: { name: 'Test Node', }, inputs: ['main'], outputs: ['main'], properties: [], usableAsTool: true, }; }); test('should return undefined when descriptionType is not manual', () => { // Arrange const newParameters: INodeParameters = { descriptionType: 'automatic', resource: 'user', operation: 'create', }; const currentParameters: INodeParameters = { descriptionType: 'automatic', resource: 'user', operation: 'create', }; // Act const result = getUpdatedToolDescription( mockNodeTypeDescription, newParameters, currentParameters, ); // Assert expect(result).toBeUndefined(); }); test('should return new description when toolDescription matches previous description', () => { // Arrange mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, ], default: 'create', }, ]; const currentParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'create', }; const newParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'update', toolDescription: 'Create a new user in Test Node', // Matches the previous description }; // Act const result = getUpdatedToolDescription( mockNodeTypeDescription, newParameters, currentParameters, ); // Assert expect(result).toBe('update user in Test Node'); }); test('should return new description when toolDescription matches node type description', () => { // Arrange const currentParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'create', }; const newParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'update', toolDescription: 'This is a test node', // Matches the node type description }; // Act const result = getUpdatedToolDescription( mockNodeTypeDescription, newParameters, currentParameters, ); // Assert expect(result).toBe('update user in Test Node'); }); test('should return undefined when toolDescription is custom', () => { // Arrange const currentParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'create', }; const newParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'update', toolDescription: 'My custom description', // Custom description }; // Act const result = getUpdatedToolDescription( mockNodeTypeDescription, newParameters, currentParameters, ); // Assert expect(result).toBeUndefined(); }); test('should return undefined for null inputs', () => { // Act const result = getUpdatedToolDescription(null, null); // Assert expect(result).toBeUndefined(); }); test('should return new description when toolDescription is empty or whitespace', () => { // Arrange const currentParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'create', }; const newParameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'update', toolDescription: ' ', // Empty/whitespace description }; // Act const result = getUpdatedToolDescription( mockNodeTypeDescription, newParameters, currentParameters, ); // Assert expect(result).toBe('update user in Test Node'); }); }); describe('getToolDescriptionForNode', () => { let mockNode: INode; let mockNodeType: INodeType; beforeEach(() => { // Arrange a basic mock node mockNode = { id: 'test-node-id', name: 'Test Node', typeVersion: 1, type: 'test-node-type', position: [0, 0], parameters: {}, }; // Arrange a basic mock node type mockNodeType = { description: { displayName: 'Test Node Type', name: 'testNodeType', icon: 'fa:test', group: ['transform'], version: 1, description: 'This is the default node description', defaults: { name: 'Test Node Type', }, inputs: ['main'], outputs: ['main'], properties: [], }, } as INodeType; }); test('should use generated description when descriptionType is auto', () => { // Arrange mockNode.parameters = { descriptionType: 'auto', resource: 'user', operation: 'create', }; mockNodeType.description.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, ], default: 'create', }, ]; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('Create a new user in Test Node Type'); }); test('should use generated description when toolDescription is empty', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', resource: 'user', operation: 'create', toolDescription: '', }; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('create user in Test Node Type'); }); test('should use generated description when toolDescription is only whitespace', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', resource: 'user', operation: 'create', toolDescription: ' ', }; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('create user in Test Node Type'); }); test('should use custom toolDescription when it exists', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', resource: 'user', operation: 'create', toolDescription: 'My custom description', }; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('My custom description'); }); test('should fall back to node type description when toolDescription is undefined', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', }; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('This is the default node description'); }); test('should resolve expression in toolDescription via the resolver callback', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', toolDescription: '={{ $json.sessionId }}{{ $json.user }}', }; const resolveToolDescription = vi.fn().mockReturnValue('123ABC'); // Act const result = getToolDescriptionForNode(mockNode, mockNodeType, resolveToolDescription); // Assert expect(resolveToolDescription).toHaveBeenCalledTimes(1); expect(result).toBe('123ABC'); }); test('should not call resolver for static toolDescription', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', toolDescription: 'Plain static description', }; const resolveToolDescription = vi.fn(); // Act const result = getToolDescriptionForNode(mockNode, mockNodeType, resolveToolDescription); // Assert expect(resolveToolDescription).not.toHaveBeenCalled(); expect(result).toBe('Plain static description'); }); test('should fall back to raw value when no resolver is provided for an expression', () => { // Arrange mockNode.parameters = { descriptionType: 'manual', toolDescription: '={{ $json.sessionId }}', }; // Act const result = getToolDescriptionForNode(mockNode, mockNodeType); // Assert expect(result).toBe('={{ $json.sessionId }}'); }); }); describe('isDefaultNodeName', () => { let mockNodeTypeDescription: INodeTypeDescription; beforeEach(() => { // Arrange a basic mock node type description mockNodeTypeDescription = { displayName: 'Test Node', name: 'testNode', icon: 'fa:test', group: ['transform'], version: 1, description: 'This is a test node', defaults: { name: 'Test Node', }, inputs: ['main'], outputs: ['main'], properties: [], usableAsTool: true, }; }); it.each([ ['Create a new user', true], ['Test Node', false], ['Test Node1', false], ['Create a new user5', true], ['Create a new user in Test Node5', false], ['Create a new user 5', false], ['Update user', false], ['Update user5', false], ['TestNode', false], ])('should detect default names for input %s', (input, expected) => { // Arrange const name = input; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, { name: 'Update', value: 'update', action: 'Update a new user', }, ], default: 'create', }, ]; const parameters: INodeParameters = { descriptionType: 'manual', resource: 'user', operation: 'create', }; // Act const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters); // Assert expect(result).toBe(expected); }); it('should detect default names for tool node types', () => { // Arrange const name = 'Create user in Test Node'; mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool]; const parameters: INodeParameters = { resource: 'user', operation: 'create', }; // Act const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters); // Assert expect(result).toBe(true); }); it('should detect non-default names for tool node types', () => { // Arrange // The default for tools would include ` in Test Node` const name = 'Create user'; mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool]; const parameters: INodeParameters = { resource: 'user', operation: 'create', }; // Act const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters); // Assert expect(result).toBe(false); }); }); describe('makeNodeName', () => { let mockNodeTypeDescription: INodeTypeDescription; beforeEach(() => { // Arrange a basic mock node type description mockNodeTypeDescription = { displayName: 'Test Node', name: 'testNode', icon: 'fa:test', group: ['transform'], version: 1, description: 'This is a test node', defaults: { name: 'Test Node', }, inputs: ['main'], outputs: ['main'], properties: [], }; }); test('should return action-based name when action is available', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, ], default: 'create', }, ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create a new user'); }); test('should check for skipNameGeneration and use displayName', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.skipNameGeneration = true; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Test Node'); }); test('should check for skipNameGeneration and use defaults.name', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.skipNameGeneration = true; mockNodeTypeDescription.defaults.name = 'Test Node Default'; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Test Node Default'); }); test('should return resource-operation-based name when action is not available', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', // No action property }, ], default: 'create', }, ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create user'); }); test('should return default name when resource or operation is missing', () => { // Arrange const nodeParameters: INodeParameters = { // No resource or operation }; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Test Node'); }); test('should handle case where nodeTypeOperation is not found', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ // No matching operation property ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create user'); }); test('should handle case where options are not a list of INodePropertyOptions', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, // Options are not INodePropertyOptions[] options: [ //@ts-expect-error {}, ], default: 'create', }, ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create user'); }); test('should handle case where node is a tool', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool]; mockNodeTypeDescription.properties = [ // No matching operation property ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert expect(result).toBe('Create user in Test Node'); }); test('should return defaults.name when skipNameGeneration is true', () => { // Arrange const nodeParameters: INodeParameters = { resource: 'user', operation: 'create', }; mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool]; mockNodeTypeDescription.skipNameGeneration = true; mockNodeTypeDescription.properties = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'Create', value: 'create', action: 'Create a new user', }, ], default: 'create', }, ]; // Act const result = makeNodeName(nodeParameters, mockNodeTypeDescription); // Assert - should return defaults.name, NOT action-based or resource/operation-based name expect(result).toBe('Test Node'); }); test.each([ ['javaScript', 'Code in JavaScript'], ['pythonNative', 'Code in Python'], ])( 'should return action-based name for Code node with %s language', (language, expectedAction) => { mockNodeTypeDescription.name = 'n8n-nodes-base.code'; mockNodeTypeDescription.properties = [ { displayName: 'Language', name: 'language', type: 'options', options: [ { name: 'JavaScript', value: 'javaScript', action: 'Code in JavaScript', }, { name: 'Python', value: 'pythonNative', action: 'Code in Python', }, ], default: 'javaScript', }, ]; const result = makeNodeName({ language }, mockNodeTypeDescription); expect(result).toBe(expectedAction); }, ); }); describe('isTool', () => { it('should return true for a node with AiTool output', () => { const description = { outputs: [NodeConnectionTypes.AiTool], version: 0, defaults: { name: '', color: '', }, inputs: [NodeConnectionTypes.Main], properties: [], displayName: '', group: [], description: '', name: 'n8n-nodes-base.someTool', }; const parameters = {}; const result = isTool(description, parameters); expect(result).toBe(true); }); it('should return true for a node with AiTool output in NodeOutputConfiguration', () => { const description = { outputs: [{ type: NodeConnectionTypes.AiTool }, { type: NodeConnectionTypes.Main }], version: 0, defaults: { name: '', color: '', }, inputs: [NodeConnectionTypes.Main], properties: [], displayName: '', group: [], description: '', name: 'n8n-nodes-base.someTool', }; const parameters = {}; const result = isTool(description, parameters); expect(result).toBe(true); }); it('returns true for a vector store node in retrieve-as-tool mode', () => { const description = { outputs: [NodeConnectionTypes.Main], version: 0, defaults: { name: '', color: '', }, inputs: [NodeConnectionTypes.Main], properties: [], displayName: '', description: '', group: [], name: 'n8n-nodes-base.vectorStore', }; const parameters = { mode: 'retrieve-as-tool' }; const result = isTool(description, parameters); expect(result).toBe(true); }); it('returns false for node with no AiTool output', () => { const description = { outputs: [NodeConnectionTypes.Main], version: 0, defaults: { name: '', color: '', }, inputs: [NodeConnectionTypes.Main], properties: [], displayName: '', group: [], description: '', name: 'n8n-nodes-base.someTool', }; const parameters = { mode: 'retrieve-as-tool' }; const result = isTool(description, parameters); expect(result).toBe(false); }); }); describe('getNodeWebhookPath', () => { const mockWorkflowId = 'workflow-123'; const mockPath = 'test-path'; it('should return path when restartWebhook is true', () => { const node = mock({ name: 'TestNode' }); const result = getNodeWebhookPath(mockWorkflowId, node, mockPath, false, true); expect(result).toBe(mockPath); }); it('should return path when node has webhookId and isFullPath is true', () => { const node = mock({ name: 'TestNode', webhookId: 'webhook-456' }); const result = getNodeWebhookPath(mockWorkflowId, node, mockPath, true, false); expect(result).toBe(mockPath); }); it('should return webhookId when node has webhookId, isFullPath is true, and path is empty', () => { const node = mock({ name: 'TestNode', webhookId: 'webhook-456' }); const result = getNodeWebhookPath(mockWorkflowId, node, '', true, false); expect(result).toBe('webhook-456'); }); it('should return webhookId/path when node has webhookId and isFullPath is false', () => { const node = mock({ name: 'TestNode', webhookId: 'webhook-456' }); const result = getNodeWebhookPath(mockWorkflowId, node, mockPath, false, false); expect(result).toBe('webhook-456/test-path'); }); it('should return workflowId/nodename/path when node has no webhookId', () => { const node = mock({ name: 'TestNode', webhookId: undefined }); const result = getNodeWebhookPath(mockWorkflowId, node, mockPath, false, false); expect(result).toBe('workflow-123/testnode/test-path'); }); }); describe('isToolType', () => { it('should return false when nodeType is undefined', () => { expect(isToolType(undefined)).toBe(false); }); it('should return false when nodeType is empty string', () => { expect(isToolType('')).toBe(false); }); it('should return true when nodeType ends with "Tool" (default includeHitl=true)', () => { expect(isToolType('CustomTool')).toBe(true); }); it('should return true when nodeType ends with "HitlTool" and includeHitl=true (default)', () => { expect(isToolType('CustomHitlTool')).toBe(true); expect(isToolType('CustomHitlTool', { includeHitl: true })).toBe(true); }); it('should return false when nodeType ends with "HitlTool" and includeHitl=false', () => { expect(isToolType('CustomHitlTool', { includeHitl: false })).toBe(false); }); it('should return true when nodeType ends with "Tool" but not "HitlTool" and includeHitl=false', () => { expect(isToolType('CustomTool', { includeHitl: false })).toBe(true); }); it('should return false when nodeType does not end with "Tool" regardless of includeHitl option', () => { expect(isToolType('CustomNode', { includeHitl: true })).toBe(false); expect(isToolType('CustomNode', { includeHitl: false })).toBe(false); }); it.each([ ['@n8n/n8n-nodes-base.toolCalculator', true], ['@n8n/n8n-nodes-base.toolCode', true], ['n8n-nodes-base.someTool', true], ['nodes-base.dot.dot.dot.someTool', true], ['nodes-base.dot.dot.dot.someTool', true], ['nodes-base.dot.dot.dot.someHitlTool', true], ])('should return true when nodeType is %s', (nodeType, expected) => { expect(isToolType(nodeType)).toBe(expected); }); }); describe('isHitlToolType', () => { it('should return false when nodeType is undefined', () => { expect(isHitlToolType(undefined)).toBe(false); }); it('should return false when nodeType is empty string', () => { expect(isHitlToolType('')).toBe(false); }); it('should return true when nodeType ends with "HitlTool"', () => { expect(isHitlToolType('CustomHitlTool')).toBe(true); }); it('should return false when nodeType ends with "Tool" but not "HitlTool"', () => { expect(isHitlToolType('CustomTool')).toBe(false); }); it('should return false when nodeType does not end with "Tool"', () => { expect(isHitlToolType('CustomNode')).toBe(false); }); }); describe('getNodeParameters - noDataExpression handling', () => { it('should strip expression prefix when noDataExpression is true and value is an expression', () => { const nodePropertiesArray: INodeProperties[] = [ { name: 'resource', displayName: 'Resource', type: 'string', default: '', noDataExpression: true, }, ]; const nodeValues: Record = { resource: '=users', }; const node: INode = { id: 'test-123', name: 'Test', type: 'n8n-nodes-base.test', typeVersion: 1, position: [0, 0], parameters: nodeValues, credentials: {}, }; const description: INodeTypeDescription = { name: 'Test', displayName: 'Test', group: [], version: 1, description: 'Test', defaults: {}, inputs: [], outputs: [], properties: nodePropertiesArray, }; const nodeType: INodeType = { description, }; const result = getNodeParameters( nodeType.description.properties, nodeValues, true, false, node, nodeType.description, ); expect(result?.resource).toBe('users'); }); it('should not strip expression prefix when noDataExpression is false', () => { const nodePropertiesArray: INodeProperties[] = [ { name: 'resource', displayName: 'Resource', type: 'string', default: '', noDataExpression: false, }, ]; const nodeValues: Record = { resource: '=users', }; const node: INode = { id: 'test-123', name: 'Test', type: 'n8n-nodes-base.test', typeVersion: 1, position: [0, 0], parameters: nodeValues, credentials: {}, }; const nodeType: INodeType = { description: { displayName: 'Test', name: 'Test', group: [], version: 1, description: 'Test', defaults: {}, inputs: [], outputs: [], properties: nodePropertiesArray, }, }; const result = getNodeParameters( nodeType.description.properties, nodeValues, true, false, node, nodeType.description, ); expect(result?.resource).toBe('=users'); }); it('should not modify non-expression values when noDataExpression is true', () => { const nodePropertiesArray: INodeProperties[] = [ { name: 'resource', displayName: 'Resource', type: 'string', default: '', noDataExpression: true, }, ]; const nodeValues: Record = { resource: 'users', }; const node: INode = { id: 'test-123', name: 'Test', type: 'n8n-nodes-base.test', typeVersion: 1, position: [0, 0], parameters: nodeValues, credentials: {}, }; const nodeType: INodeType = { description: { displayName: 'Test', name: 'Test', group: [], version: 1, description: 'Test', defaults: {}, inputs: [], outputs: [], properties: nodePropertiesArray, }, }; const result = getNodeParameters( nodeType.description.properties, nodeValues, true, false, node, nodeType.description, ); expect(result?.resource).toBe('users'); }); it('should handle undefined values when noDataExpression is true', () => { const nodePropertiesArray: INodeProperties[] = [ { name: 'resource', displayName: 'Resource', type: 'string', default: '', noDataExpression: true, }, ]; const nodeValues: NodeParameterValueType = { resource: undefined, }; const node: INode = { id: 'test-123', name: 'Test', type: 'n8n-nodes-base.test', typeVersion: 1, position: [0, 0], parameters: nodeValues, credentials: {}, }; const nodeType: INodeType = { description: { displayName: 'Test', name: 'Test', group: [], version: 1, description: 'Test', defaults: {}, inputs: [], outputs: [], properties: nodePropertiesArray, }, }; const result = getNodeParameters( nodeType.description.properties, nodeValues, true, false, node, nodeType.description, ); // When undefined, the default value (empty string) is used expect(result?.resource).toBe(''); }); describe('$fromAI placeholder carve-out', () => { const resolveQuery = (query: string) => { const properties: INodeProperties[] = [ { name: 'query', displayName: 'Query', type: 'string', default: '', noDataExpression: true, }, ]; const nodeValues: Record = { query }; const node: INode = { id: 'test-123', name: 'Test', type: 'n8n-nodes-base.test', typeVersion: 1, position: [0, 0], parameters: nodeValues, credentials: {}, }; const description: INodeTypeDescription = { displayName: 'Test', name: 'Test', group: [], version: 1, description: 'Test', defaults: {}, inputs: [], outputs: [], properties, }; return getNodeParameters(properties, nodeValues, true, false, node, description)?.query; }; it('keeps a lone $fromAI() placeholder intact', () => { const query = "=$fromAI('sqlQuery', 'The SQL query to execute', 'string')"; expect(resolveQuery(query)).toBe(query); }); it('keeps a $fromAI() placeholder wrapped in {{ }} intact', () => { const query = "={{ $fromAI('sqlQuery') }}"; expect(resolveQuery(query)).toBe(query); }); it('still strips a plain expression that is not a $fromAI() call', () => { expect(resolveQuery('=$env.SECRET')).toBe('$env.SECRET'); }); it('still strips $fromAI() concatenated with another expression', () => { expect(resolveQuery("=$fromAI('x') + $env.SECRET")).toBe("$fromAI('x') + $env.SECRET"); }); it('still strips $fromAI() with an interpolated value in its argument', () => { // Split the `${` token so the lint rule doesn't read it as interpolation. const interpolated = '`$' + '{$env.SECRET}`'; expect(resolveQuery(`=$fromAI(${interpolated})`)).toBe(`$fromAI(${interpolated})`); }); }); }); describe('getNodeParameters filter defaults', () => { const filterProperty: INodeProperties = { displayName: 'Conditions', name: 'conditions', type: 'filter', default: {}, }; it('should add missing combinator and options to a filter parameter', () => { const result = getNodeParameters( [filterProperty], { conditions: { conditions: [ { leftValue: '={{ $json.field }}', rightValue: 'value', operator: { type: 'string', operation: 'equals' }, }, ], }, }, true, false, null, null, ); expect(result).toEqual({ conditions: { combinator: 'and', options: { caseSensitive: true, leftValue: '', typeValidation: 'strict', version: 1, }, conditions: [ { leftValue: '={{ $json.field }}', rightValue: 'value', operator: { type: 'string', operation: 'equals' }, }, ], }, }); }); it('should preserve existing combinator and options', () => { const result = getNodeParameters( [filterProperty], { conditions: { combinator: 'or', options: { caseSensitive: false, leftValue: '', typeValidation: 'loose', version: 2, }, conditions: [], }, }, true, false, null, null, ); expect(result).toEqual({ conditions: { combinator: 'or', options: { caseSensitive: false, leftValue: '', typeValidation: 'loose', version: 2, }, conditions: [], }, }); }); it('should fill in missing fields in partial options', () => { const result = getNodeParameters( [filterProperty], { conditions: { combinator: 'and', options: { caseSensitive: false }, conditions: [], }, }, true, false, null, null, ); expect(result).toEqual({ conditions: { combinator: 'and', options: { caseSensitive: false, leftValue: '', typeValidation: 'strict', version: 1, }, conditions: [], }, }); }); }); describe('findDisplayedProperty', () => { const routingFor = (url: string) => ({ typeOptions: { loadOptions: { routing: { request: { url } } } }, }); const resolve = ( path: string, properties: INodeProperties[], nodeValues: INodeParameters = {}, node: Pick | null = null, ) => findDisplayedProperty(path, properties, nodeValues, node, null) as | INodeProperties | undefined; const routingUrl = (property: INodeProperties | undefined) => property?.typeOptions?.loadOptions?.routing?.request?.url; it('resolves a top-level property', () => { const properties = [ { displayName: 'Model', name: 'model', type: 'options', default: '', ...routingFor('/models'), }, ] as INodeProperties[]; expect(resolve('parameters.model', properties)?.name).toBe('model'); }); it('resolves a property nested in a collection', () => { const properties = [ { displayName: 'Options', name: 'options', type: 'collection', default: {}, options: [ { displayName: 'Model', name: 'model', type: 'options', default: '', ...routingFor('/m'), }, ], }, ] as INodeProperties[]; expect(resolve('parameters.options.model', properties)?.name).toBe('model'); }); it('resolves a field in a fixedCollection section, ignoring the array index', () => { const properties = [ { displayName: 'Create', name: 'createContactAttributes', type: 'fixedCollection', default: {}, options: [ { displayName: 'Attr', name: 'attributesValues', values: [ { displayName: 'Field', name: 'fieldName', type: 'options', default: '', ...routingFor('/create'), }, ], }, ], }, ] as INodeProperties[]; expect( routingUrl( resolve('parameters.createContactAttributes.attributesValues[0].fieldName', properties), ), ).toBe('/create'); }); it('picks the displayed variant when two properties share a name (param-gated)', () => { const properties = [ { displayName: 'Model', name: 'model', type: 'options', default: '', displayOptions: { show: { modelSource: ['a'] } }, ...routingFor('/a'), }, { displayName: 'Model', name: 'model', type: 'options', default: '', displayOptions: { show: { modelSource: ['b'] } }, ...routingFor('/b'), }, ] as INodeProperties[]; expect(routingUrl(resolve('parameters.model', properties, { modelSource: 'a' }))).toBe('/a'); expect(routingUrl(resolve('parameters.model', properties, { modelSource: 'b' }))).toBe('/b'); }); it('picks the displayed variant by node type version', () => { const properties = [ { displayName: 'Model', name: 'model', type: 'options', default: '', displayOptions: { show: { '@version': [1] } }, ...routingFor('/v1'), }, { displayName: 'Model', name: 'model', type: 'options', default: '', displayOptions: { show: { '@version': [2] } }, ...routingFor('/v2'), }, ] as INodeProperties[]; expect(routingUrl(resolve('parameters.model', properties, {}, { typeVersion: 2 }))).toBe( '/v2', ); }); it('returns undefined for an unknown path', () => { const properties = [ { displayName: 'Model', name: 'model', type: 'options', default: '', ...routingFor('/models'), }, ] as INodeProperties[]; expect(resolve('parameters.unknown', properties)).toBeUndefined(); }); it('returns undefined when a collection property has no options', () => { const properties = [ { displayName: 'Options', name: 'options', type: 'collection', default: {} }, ] as INodeProperties[]; expect(resolve('parameters.options.model', properties)).toBeUndefined(); }); }); describe('isTriggerNodeType', () => { // Membership in TRIGGER_NODE_TYPES: each legacy/special type is matched by // identity, not by the generic "contains trigger" heuristic. test.each([ 'n8n-nodes-base.webhook', 'n8n-nodes-base.cron', 'n8n-nodes-base.emailReadImap', 'n8n-nodes-base.telegramBot', 'n8n-nodes-base.start', ])('recognises the explicitly-listed trigger type %s', (type) => { expect(isTriggerNodeType(type)).toBe(true); }); it('recognises any type whose name contains "trigger"', () => { expect(isTriggerNodeType('n8n-nodes-base.scheduleTrigger')).toBe(true); expect(isTriggerNodeType('n8n-nodes-base.manualTrigger')).toBe(true); }); it('matches "trigger" case-insensitively', () => { // Guards the `.toLowerCase()` normalisation: without it these would miss. expect(isTriggerNodeType('CustomTrigger')).toBe(true); expect(isTriggerNodeType('SOMETRIGGERNODE')).toBe(true); }); it('returns false for non-trigger types not in the set', () => { expect(isTriggerNodeType('n8n-nodes-base.set')).toBe(false); expect(isTriggerNodeType('n8n-nodes-base.httpRequest')).toBe(false); expect(isTriggerNodeType('')).toBe(false); }); }); describe('getParameterIssues - validation paths', () => { const testNode: INode = { id: '12345', name: 'Test Node', typeVersion: 1, type: 'n8n-nodes-base.testNode', position: [1, 1], parameters: {}, }; describe('resourceLocator regex validation', () => { const resourceLocatorProperties: INodeProperties = { displayName: 'Document', name: 'documentId', type: 'resourceLocator', default: { mode: 'id', value: '' }, modes: [ { displayName: 'By ID', name: 'id', type: 'string', validation: [ { type: 'regex', properties: { regex: '[0-9]+', errorMessage: 'Document ID must be numeric', }, }, ], }, ], }; it('reports the configured error when the value fails the mode regex', () => { const result = getParameterIssues( resourceLocatorProperties, { documentId: { __rl: true, mode: 'id', value: 'abc' } }, '', testNode, null, ); expect(result).toEqual({ parameters: { documentId: ['Document ID must be numeric'] }, }); }); it('returns no issues when the value matches the mode regex', () => { const result = getParameterIssues( resourceLocatorProperties, { documentId: { __rl: true, mode: 'id', value: '123' } }, '', testNode, null, ); expect(result).toEqual({}); }); it('skips validation for expression values (starting with "=")', () => { // Metamorphic: an expression is resolved at runtime, so static // validation must never flag it regardless of the regex. const result = getParameterIssues( resourceLocatorProperties, { documentId: { __rl: true, mode: 'id', value: '={{ $json.id }}' } }, '', testNode, null, ); expect(result).toEqual({}); }); it('returns no issues when the value mode has no matching mode definition', () => { const result = getParameterIssues( resourceLocatorProperties, { documentId: { __rl: true, mode: 'url', value: 'abc' } }, '', testNode, null, ); expect(result).toEqual({}); }); it('applies regex validation to workflowSelector values too', () => { // workflowSelector shares the resource-locator validation branch. const workflowSelectorProperties: INodeProperties = { ...resourceLocatorProperties, name: 'workflowId', type: 'workflowSelector', }; const result = getParameterIssues( workflowSelectorProperties, { workflowId: { __rl: true, mode: 'id', value: 'abc' } }, '', testNode, null, ); expect(result).toEqual({ parameters: { workflowId: ['Document ID must be numeric'] }, }); }); }); describe('resourceMapper required-field validation', () => { const resourceMapperProperties: INodeProperties = { displayName: 'Columns', name: 'columns', type: 'resourceMapper', default: {}, typeOptions: { resourceMapper: { // `add` mode is the only one that runs the required-field check. mode: 'add', resourceMapperMethod: 'getFields', fieldWords: { singular: 'field', plural: 'fields' }, }, }, }; it('reports required schema fields that are missing in add mode', () => { const result = getParameterIssues( resourceMapperProperties, { columns: { mappingMode: 'defineBelow', value: { providedField: 'x' }, schema: [ { id: 'requiredField', displayName: 'requiredField', required: true, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true, }, ], }, }, '', testNode, null, ); expect(result.parameters?.['columns.requiredField']).toContain( 'Field "requiredField" is required', ); }); it('skips the required-field check outside add mode', () => { // Only `add` mode validates required fields; update/upsert/map map onto // existing rows where missing fields are legitimate. const updateModeProperties: INodeProperties = { ...resourceMapperProperties, typeOptions: { resourceMapper: { mode: 'update', resourceMapperMethod: 'getFields', fieldWords: { singular: 'field', plural: 'fields' }, }, }, }; const result = getParameterIssues( updateModeProperties, { columns: { mappingMode: 'defineBelow', value: { providedField: 'x' }, schema: [ { id: 'requiredField', displayName: 'requiredField', required: true, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true, }, ], }, }, '', testNode, null, ); expect(result.parameters?.['columns.requiredField']).toBeUndefined(); }); it('reports a type mismatch for a provided field value', () => { const result = getParameterIssues( resourceMapperProperties, { columns: { mappingMode: 'defineBelow', value: { numField: 'not-a-number' }, schema: [ { id: 'numField', displayName: 'numField', required: false, defaultMatch: false, display: true, type: 'number', canBeUsedToMatch: true, }, ], }, }, '', testNode, null, ); expect(result.parameters?.['columns.numField']).toBeDefined(); expect(result.parameters?.['columns.numField']?.length).toBeGreaterThan(0); }); it('skips type validation for expression field values', () => { // Metamorphic: an expression value is resolved at runtime, so a static // type mismatch must not be reported. const result = getParameterIssues( resourceMapperProperties, { columns: { mappingMode: 'defineBelow', value: { numField: '={{ $json.n }}' }, schema: [ { id: 'numField', displayName: 'numField', required: false, defaultMatch: false, display: true, type: 'number', canBeUsedToMatch: true, }, ], }, }, '', testNode, null, ); expect(result).toEqual({}); }); it('does not validate in automatic mapping mode', () => { // Metamorphic: autoMapInputData has no user-entered values to validate, // so it must never produce issues regardless of the schema. const result = getParameterIssues( resourceMapperProperties, { columns: { mappingMode: 'autoMapInputData', value: null, schema: [ { id: 'requiredField', displayName: 'requiredField', required: true, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true, }, ], }, }, '', testNode, null, ); expect(result).toEqual({}); }); }); describe('validateType', () => { const numberProperties: INodeProperties = { displayName: 'Limit', name: 'limit', type: 'number', default: 0, validateType: 'number', }; it('reports an issue when the value does not match validateType', () => { const result = getParameterIssues( numberProperties, { limit: 'not-a-number' }, '', testNode, null, ); expect(result.parameters?.limit).toHaveLength(1); }); it('returns no issues when the value matches validateType', () => { const result = getParameterIssues(numberProperties, { limit: 5 }, '', testNode, null); expect(result).toEqual({}); }); it('skips validateType for expression values (starting with "=")', () => { const result = getParameterIssues( numberProperties, { limit: '={{ $json.count }}' }, '', testNode, null, ); expect(result).toEqual({}); }); }); describe('required parameter emptiness by type', () => { const requiredOf = (overrides: Partial): INodeProperties => ({ displayName: 'Field', name: 'field', type: 'string', default: '', required: true, ...overrides, }); it('flags a required string that is an empty string', () => { const result = getParameterIssues(requiredOf({}), { field: '' }, '', testNode, null); expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); it('flags a required string that is undefined', () => { const result = getParameterIssues(requiredOf({}), { field: undefined }, '', testNode, null); expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); it('flags a required multiOptions with an empty array', () => { const result = getParameterIssues( requiredOf({ type: 'multiOptions', options: [], default: [] }), { field: [] }, '', testNode, null, ); expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); it('flags a required options parameter that is an empty string', () => { const result = getParameterIssues( requiredOf({ type: 'options', options: [] }), { field: '' }, '', testNode, null, ); expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); it('flags a required resourceLocator with an empty value', () => { const result = getParameterIssues( requiredOf({ type: 'resourceLocator', default: { mode: 'id', value: '' } }), { field: { __rl: true, mode: 'id', value: '' } }, '', testNode, null, ); expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); it('does not flag a required string that has a value', () => { const result = getParameterIssues(requiredOf({}), { field: 'present' }, '', testNode, null); expect(result).toEqual({}); }); it('checks each entry of a required multipleValues parameter', () => { const result = getParameterIssues( requiredOf({ typeOptions: { multipleValues: true }, default: [] }), { field: ['ok', ''] }, '', testNode, null, ); // Only the empty entry is flagged. expect(result).toEqual({ parameters: { field: ['Parameter "Field" is required.'] } }); }); }); it('recurses into collection children and validates required sub-fields', () => { const collectionProperties: INodeProperties = { displayName: 'Options', name: 'options', type: 'collection', default: {}, options: [ { displayName: 'Required Field', name: 'reqField', type: 'string', default: '', required: true, }, ], }; const result = getParameterIssues(collectionProperties, { reqField: '' }, '', testNode, null); expect(result).toEqual({ parameters: { reqField: ['Parameter "Required Field" is required.'] }, }); }); }); });