mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
fix(editor): Stabilize workflow setup credentials state (#30513)
This commit is contained in:
+1
@@ -51,6 +51,7 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
|
||||
vi.mock('@/features/settings/environments.ee/environments.store', () => ({
|
||||
default: () => ({ variablesAsObject: {} }),
|
||||
useEnvironmentsStore: () => ({ variablesAsObject: {} }),
|
||||
}));
|
||||
|
||||
function makeContext(): WorkflowSetupContext {
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
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 { INodeUi } from '@/Interface';
|
||||
import WorkflowSetupSectionBody from './WorkflowSetupSectionBody.vue';
|
||||
import { makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import type { WorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
|
||||
const workflowSetupContext = vi.hoisted(() => ({
|
||||
current: undefined as unknown as WorkflowSetupContext,
|
||||
}));
|
||||
|
||||
const credentialsStore = vi.hoisted(() => ({
|
||||
getCredentialById: vi.fn(),
|
||||
}));
|
||||
|
||||
const nodeTypesStore = vi.hoisted(() => ({
|
||||
getNodeType: vi.fn(),
|
||||
getAllNodeTypes: vi.fn(),
|
||||
}));
|
||||
|
||||
const renderedCredentials = vi.hoisted(() => [] as unknown[]);
|
||||
|
||||
vi.mock('../composables/useWorkflowSetupContext', () => ({
|
||||
useWorkflowSetupContext: () => workflowSetupContext.current,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => credentialsStore,
|
||||
}));
|
||||
|
||||
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'],
|
||||
template: '<div data-test-id="node-credentials"><slot name="label-postfix" /></div>',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue');
|
||||
|
||||
return {
|
||||
default: defineComponent({
|
||||
props: ['node'],
|
||||
emits: ['valueChanged', 'parameterBlur'],
|
||||
setup(props, { emit }) {
|
||||
return () => {
|
||||
renderedCredentials.push((props.node as INodeUi | undefined)?.credentials);
|
||||
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
'data-test-id': 'change-parameter',
|
||||
onClick: () =>
|
||||
emit('valueChanged', {
|
||||
name: 'parameters.formId',
|
||||
value: 'form-1',
|
||||
}),
|
||||
},
|
||||
'Change parameter',
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowSetupSectionBody);
|
||||
|
||||
function makeContext(section: WorkflowSetupSection): WorkflowSetupContext {
|
||||
const parameters = ref({ formId: '' });
|
||||
|
||||
return {
|
||||
sections: computed(() => [section]),
|
||||
steps: computed(() => [{ kind: 'section', section }]),
|
||||
currentStepIndex: ref(0),
|
||||
activeStep: computed(() => ({ kind: 'section', section })),
|
||||
hasOtherUnhandledSteps: computed(() => false),
|
||||
canAdvanceToNextIncomplete: computed(() => false),
|
||||
credentialSelections: ref({ [section.targetNodeName]: { typeformApi: 'cred-1' } }),
|
||||
terminalState: ref(null),
|
||||
isReady: ref(true),
|
||||
projectId: computed(() => 'project-1'),
|
||||
credentialFlow: computed(() => undefined),
|
||||
isActionPending: ref(false),
|
||||
setCredential: vi.fn(),
|
||||
setParameterValue: vi.fn((_setupSection, parameterName: string, value: unknown) => {
|
||||
parameters.value = { ...parameters.value, [parameterName]: value };
|
||||
}),
|
||||
getDisplayNode: (setupSection) =>
|
||||
({
|
||||
...setupSection.node,
|
||||
parameters: parameters.value,
|
||||
}) as INodeUi,
|
||||
isSectionComplete: () => false,
|
||||
isCredentialTestFailed: () => false,
|
||||
isSectionSkipped: () => false,
|
||||
isStepComplete: () => false,
|
||||
isStepSkipped: () => false,
|
||||
isStepHandled: () => false,
|
||||
goToStep: vi.fn(),
|
||||
goToNext: vi.fn(),
|
||||
goToPrev: vi.fn(),
|
||||
goToNextIncomplete: vi.fn(),
|
||||
apply: vi.fn(async () => {}),
|
||||
skipCurrentStep: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('WorkflowSetupSectionBody', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
renderedCredentials.length = 0;
|
||||
credentialsStore.getCredentialById.mockReturnValue({ id: 'cred-1', name: 'Typeform account' });
|
||||
nodeTypesStore.getNodeType.mockReturnValue({
|
||||
name: 'n8n-nodes-base.typeformTrigger',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Form Name or ID',
|
||||
name: 'formId',
|
||||
type: 'options',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
nodeTypesStore.getAllNodeTypes.mockReturnValue({
|
||||
nodeTypes: {},
|
||||
init: async () => {},
|
||||
getByNameAndVersion: () => undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the synthetic credentials object stable when parameter values change', async () => {
|
||||
const section = makeWorkflowSetupSection({
|
||||
id: 'Typeform Trigger:typeformApi',
|
||||
targetNodeName: 'Typeform Trigger',
|
||||
credentialType: 'typeformApi',
|
||||
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();
|
||||
|
||||
const credentialsBeforeParameterChange = renderedCredentials.at(-1);
|
||||
await fireEvent.click(getByTestId('change-parameter'));
|
||||
await nextTick();
|
||||
|
||||
expect(renderedCredentials.at(-1)).toBe(credentialsBeforeParameterChange);
|
||||
});
|
||||
});
|
||||
+13
-5
@@ -6,7 +6,7 @@ import NodeCredentials from '@/features/credentials/components/NodeCredentials.v
|
||||
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 { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
import { Workflow, type IConnections, type INodeProperties } from 'n8n-workflow';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
@@ -32,6 +32,17 @@ const selectedCredentialId = computed(() =>
|
||||
: null,
|
||||
);
|
||||
|
||||
const selectedCredentials = computed<INodeUi['credentials']>(() => {
|
||||
const type = credentialType.value;
|
||||
if (!type) return undefined;
|
||||
|
||||
const cred = selectedCredentialId.value
|
||||
? credentialsStore.getCredentialById(selectedCredentialId.value)
|
||||
: undefined;
|
||||
|
||||
return cred ? { [type]: { id: cred.id, name: cred.name } } : {};
|
||||
});
|
||||
|
||||
const targetNodeNames = computed(() =>
|
||||
props.section.credentialTargetNodes.map((node) => node.name),
|
||||
);
|
||||
@@ -79,12 +90,9 @@ function getRootParameterName(parameterName: string) {
|
||||
const displayNode = computed<INodeUi>(() => {
|
||||
const node = ctx.getDisplayNode(props.section);
|
||||
if (!credentialType.value) return node;
|
||||
const cred = selectedCredentialId.value
|
||||
? credentialsStore.getCredentialById(selectedCredentialId.value)
|
||||
: undefined;
|
||||
return {
|
||||
...node,
|
||||
credentials: cred ? { [credentialType.value]: { id: cred.id, name: cred.name } } : {},
|
||||
credentials: selectedCredentials.value,
|
||||
} as INodeUi;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user