fix(editor): Fix wf context for ai assistant setup node validations (#30664)

This commit is contained in:
Svetoslav Dekov
2026-05-18 21:37:31 +03:00
committed by GitHub
parent ac02d32aa9
commit f2a86e9a1d
8 changed files with 90 additions and 36 deletions
@@ -1,3 +1,4 @@
import { createTestingPinia } from '@pinia/testing';
import { computed, ref } from 'vue';
import { mount } from '@vue/test-utils';
import { describe, expect, it, vi } from 'vitest';
@@ -50,6 +51,11 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({
useNodeTypesStore: () => ({
getNodeType: () => null,
communityNodeType: () => null,
getAllNodeTypes: () => ({
nodeTypes: {},
init: async () => {},
getByNameAndVersion: () => undefined,
}),
}),
}));
@@ -69,6 +75,7 @@ function makeContext(): WorkflowSetupContext {
credentialSelections: ref({}),
terminalState: ref(null),
isReady: ref(true),
workflowId: computed(() => undefined),
projectId: computed(() => undefined),
credentialFlow: computed(() => undefined),
isActionPending: ref(false),
@@ -96,6 +103,7 @@ function renderComponent(section: WorkflowSetupSection) {
return mount(WorkflowSetupSectionBody, {
props: { section },
global: {
plugins: [createTestingPinia({ stubActions: false })],
stubs: {
N8nText: { template: '<span><slot /></span>' },
N8nTooltip: {
@@ -104,6 +104,7 @@ function makeContext(isComplete: Ref<boolean>, options: ContextOptions = {}): Wo
credentialSelections: ref({}),
terminalState: ref(null),
isReady: ref(true),
workflowId: computed(() => undefined),
projectId: computed(() => undefined),
credentialFlow: computed(() => undefined),
isActionPending: options.isActionPending ?? ref(false),
@@ -83,6 +83,7 @@ function makeContext(section: WorkflowSetupSection): WorkflowSetupContext {
credentialSelections: ref({}),
terminalState: ref(null),
isReady: ref(true),
workflowId: computed(() => undefined),
projectId: computed(() => undefined),
credentialFlow: computed(() => undefined),
isActionPending: ref(false),
@@ -64,6 +64,7 @@ function makeContext(overrides: ContextOverrides = {}): WorkflowSetupContext {
credentialSelections: ref({}),
terminalState: ref(null),
isReady: ref(true),
workflowId: computed(() => undefined),
projectId: computed(() => undefined),
credentialFlow: computed(() => undefined),
isActionPending: ref(false),
@@ -1,7 +1,9 @@
import { createTestingPinia } from '@pinia/testing';
import { computed, nextTick, ref } from 'vue';
import { fireEvent } from '@testing-library/vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createComponentRenderer } from '@/__tests__/render';
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { INodeUi } from '@/Interface';
import WorkflowSetupSectionBody from './WorkflowSetupSectionBody.vue';
import { makeWorkflowSetupSection } from '../__tests__/factories';
@@ -22,6 +24,9 @@ const nodeTypesStore = vi.hoisted(() => ({
}));
const renderedCredentials = vi.hoisted(() => [] as unknown[]);
const workflowDocumentStoreRef = vi.hoisted(() => ({
current: null as WorkflowDocumentStore | null,
}));
vi.mock('../composables/useWorkflowSetupContext', () => ({
useWorkflowSetupContext: () => workflowSetupContext.current,
@@ -35,11 +40,6 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({
useNodeTypesStore: () => nodeTypesStore,
}));
vi.mock('@/features/settings/environments.ee/environments.store', () => ({
default: () => ({ variablesAsObject: {} }),
useEnvironmentsStore: () => ({ variablesAsObject: {} }),
}));
vi.mock('@/features/credentials/components/NodeCredentials.vue', () => ({
default: {
props: ['node'],
@@ -52,13 +52,17 @@ vi.mock('@/app/components/FreeAiCreditsCallout.vue', () => ({
}));
vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async () => {
const { defineComponent, h } = await import('vue');
const { defineComponent, h, inject } = await import('vue');
const { WorkflowDocumentStoreKey } = await import('@/app/constants/injectionKeys');
return {
default: defineComponent({
props: ['node'],
emits: ['valueChanged', 'parameterBlur'],
setup(props, { emit }) {
const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null);
workflowDocumentStoreRef.current = workflowDocumentStore?.value ?? null;
return () => {
renderedCredentials.push((props.node as INodeUi | undefined)?.credentials);
@@ -80,7 +84,9 @@ vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async ()
};
});
const renderComponent = createComponentRenderer(WorkflowSetupSectionBody);
const renderComponent = createComponentRenderer(WorkflowSetupSectionBody, {
pinia: createTestingPinia({ stubActions: false }),
});
function makeContext(section: WorkflowSetupSection): WorkflowSetupContext {
const parameters = ref({ formId: '' });
@@ -95,6 +101,7 @@ function makeContext(section: WorkflowSetupSection): WorkflowSetupContext {
credentialSelections: ref({ [section.targetNodeName]: { typeformApi: 'cred-1' } }),
terminalState: ref(null),
isReady: ref(true),
workflowId: computed(() => 'workflow-1'),
projectId: computed(() => 'project-1'),
credentialFlow: computed(() => undefined),
isActionPending: ref(false),
@@ -126,6 +133,7 @@ describe('WorkflowSetupSectionBody', () => {
beforeEach(() => {
vi.clearAllMocks();
renderedCredentials.length = 0;
workflowDocumentStoreRef.current = null;
credentialsStore.getCredentialById.mockReturnValue({ id: 'cred-1', name: 'Typeform account' });
nodeTypesStore.getNodeType.mockReturnValue({
name: 'n8n-nodes-base.typeformTrigger',
@@ -170,4 +178,37 @@ describe('WorkflowSetupSectionBody', () => {
expect(renderedCredentials.at(-1)).toBe(credentialsBeforeParameterChange);
});
it('provides a scoped workflow document store with the display node', async () => {
const section = makeWorkflowSetupSection({
id: 'Typeform Trigger:typeformApi',
targetNodeName: 'Typeform Trigger',
parameterNames: ['formId'],
node: {
id: 'typeform-trigger',
name: 'Typeform Trigger',
type: 'n8n-nodes-base.typeformTrigger',
typeVersion: 1,
parameters: { formId: '' },
},
});
workflowSetupContext.current = makeContext(section);
const { getByTestId } = renderComponent({ props: { section } });
await nextTick();
expect(workflowDocumentStoreRef.current?.documentId).toBe(
'workflow-1@Typeform Trigger:typeformApi',
);
expect(workflowDocumentStoreRef.current?.getNodeByName('Typeform Trigger')?.parameters).toEqual(
{ formId: '' },
);
await fireEvent.click(getByTestId('change-parameter'));
await nextTick();
expect(workflowDocumentStoreRef.current?.getNodeByName('Typeform Trigger')?.parameters).toEqual(
{ formId: 'form-1' },
);
});
});
@@ -7,9 +7,12 @@ import FreeAiCreditsCallout from '@/app/components/FreeAiCreditsCallout.vue';
import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
import { Workflow, type IConnections, type INodeProperties } from 'n8n-workflow';
import { ExpressionLocalResolveContextSymbol, WorkflowDocumentStoreKey } from '@/app/constants';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import type { INodeProperties } from 'n8n-workflow';
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface';
import type { WorkflowSetupSection } from '../workflowSetup.types';
@@ -23,7 +26,6 @@ const ctx = useWorkflowSetupContext();
const i18n = useI18n();
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
const environmentsStore = useEnvironmentsStore();
const credentialType = computed(() => props.section.credentialType);
@@ -97,30 +99,31 @@ const displayNode = computed<INodeUi>(() => {
} as INodeUi;
});
const expressionContext = computed<ExpressionLocalResolveContext | undefined>(() => {
const node = displayNode.value;
const connections: IConnections = {};
const workflow = new Workflow({
id: 'instance-ai-workflow-setup',
name: 'Instance AI workflow setup',
nodes: [node],
connections,
active: false,
nodeTypes: nodeTypesStore.getAllNodeTypes(),
});
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(
createWorkflowDocumentId(
ctx.workflowId.value ?? 'instance-ai-workflow-setup',
props.section.id,
),
),
);
return {
localResolve: true,
envVars: environmentsStore.variablesAsObject,
workflow,
execution: null,
nodeName: node.name,
additionalKeys: {},
connections,
};
});
watch(
displayNode,
(node) => {
workflowDocumentStore.value.setNodes([node]);
},
{ immediate: true, deep: true },
);
const expressionContext = computed<ExpressionLocalResolveContext | undefined>(() => ({
localResolve: true,
nodeName: displayNode.value.name,
additionalKeys: {},
}));
provide(ExpressionLocalResolveContextSymbol, expressionContext);
provide(WorkflowDocumentStoreKey, workflowDocumentStore);
function onCredentialSelected(update: INodeUpdatePropertiesInformation) {
if (!credentialType.value) return;
@@ -36,6 +36,7 @@ export interface WorkflowSetupContext {
credentialSelections: Ref<CredentialSelectionsMap>;
terminalState: Ref<TerminalState | null>;
isReady: Ref<boolean>;
workflowId: ComputedRef<string | undefined>;
projectId: ComputedRef<string | undefined>;
credentialFlow: ComputedRef<InstanceAiCredentialFlow | undefined>;
isActionPending: Ref<boolean>;
@@ -92,6 +93,7 @@ export function provideWorkflowSetupContext(opts: ProvideOptions): WorkflowSetup
const inputsState = useWorkflowSetupInputs({ sections });
const workflowId = computed(() => opts.workflowId.value);
const projectId = computed(() => opts.projectId.value);
const credentialFlow = computed(() => opts.credentialFlow.value);
@@ -171,6 +173,7 @@ export function provideWorkflowSetupContext(opts: ProvideOptions): WorkflowSetup
credentialSelections: inputsState.credentialSelections,
terminalState: applyMachine.terminalState,
isReady: bootstrap.isReady,
workflowId,
projectId,
credentialFlow,
isActionPending: actions.isActionPending,
@@ -774,10 +774,6 @@ test.describe(
test('should clear required parameter issue indicator when the field is filled', async ({
n8n,
}) => {
test.fixme(
true,
'Broken by ParameterInput node-resolution refactor in #30219 — issues are computed against the global workflow document store node instead of the wizard displayNode.',
);
await n8n.api.workflows.createWorkflow(
createParameterOnlyWorkflow(PARAMETER_ISSUE_WORKFLOW_NAME),
);