mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
refactor(editor): Use typed-mocks to speed up tests and type-checking (no-changelog) (#9796)
This commit is contained in:
parent
41e06be6fd
commit
e3e20b48eb
|
@ -1,59 +1,4 @@
|
|||
import type { INodeTypeData, INodeTypeDescription, IN8nUISettings } from 'n8n-workflow';
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
SET_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import nodeTypesJson from '../../../nodes-base/dist/types/nodes.json';
|
||||
import aiNodeTypesJson from '../../../@n8n/nodes-langchain/dist/types/nodes.json';
|
||||
|
||||
const allNodeTypes = [...nodeTypesJson, ...aiNodeTypesJson];
|
||||
|
||||
export function findNodeTypeDescriptionByName(name: string): INodeTypeDescription {
|
||||
return allNodeTypes.find((node) => node.name === name) as INodeTypeDescription;
|
||||
}
|
||||
|
||||
export const testingNodeTypes: INodeTypeData = {
|
||||
[MANUAL_TRIGGER_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeTypeDescriptionByName(MANUAL_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[SET_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeTypeDescriptionByName(SET_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[CHAT_TRIGGER_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeTypeDescriptionByName(CHAT_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[AGENT_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeTypeDescriptionByName(AGENT_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultMockNodeTypes: INodeTypeData = {
|
||||
[MANUAL_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_TRIGGER_NODE_TYPE],
|
||||
[SET_NODE_TYPE]: testingNodeTypes[SET_NODE_TYPE],
|
||||
};
|
||||
|
||||
export function mockNodeTypesToArray(nodeTypes: INodeTypeData): INodeTypeDescription[] {
|
||||
return Object.values(nodeTypes).map(
|
||||
(nodeType) => nodeType.type.description as INodeTypeDescription,
|
||||
);
|
||||
}
|
||||
|
||||
export const defaultMockNodeTypesArray: INodeTypeDescription[] =
|
||||
mockNodeTypesToArray(defaultMockNodeTypes);
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
|
||||
export const defaultSettings: IN8nUISettings = {
|
||||
allowedModules: {},
|
||||
|
|
|
@ -2,48 +2,76 @@ import type {
|
|||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IVersionedNodeType,
|
||||
IConnections,
|
||||
IDataObject,
|
||||
INode,
|
||||
IPinData,
|
||||
IWorkflowSettings,
|
||||
LoadedClass,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers, Workflow } from 'n8n-workflow';
|
||||
import { uuid } from '@jsplumb/util';
|
||||
import { defaultMockNodeTypes } from '@/__tests__/defaults';
|
||||
import type { INodeUi, ITag, IUsedCredential, IWorkflowDb, WorkflowMetadata } from '@/Interface';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes {
|
||||
const nodeTypes = {
|
||||
...defaultMockNodeTypes,
|
||||
...Object.keys(data).reduce<INodeTypeData>((acc, key) => {
|
||||
acc[key] = data[key];
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
CODE_NODE_TYPE,
|
||||
EXECUTABLE_TRIGGER_NODE_TYPES,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
NO_OP_NODE_TYPE,
|
||||
SET_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
|
||||
const mockNode = (name: string, type: string, props: Partial<INode> = {}) =>
|
||||
mock<INode>({ name, type, ...props });
|
||||
|
||||
const mockLoadedClass = (name: string) =>
|
||||
mock<LoadedClass<INodeType>>({
|
||||
type: mock<INodeType>({
|
||||
// @ts-expect-error
|
||||
description: mock<INodeTypeDescription>({
|
||||
name,
|
||||
displayName: name,
|
||||
version: 1,
|
||||
properties: [],
|
||||
group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [],
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
documentationUrl: 'https://docs',
|
||||
webhooks: undefined,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const mockNodes = [
|
||||
mockNode('Manual Trigger', MANUAL_TRIGGER_NODE_TYPE),
|
||||
mockNode('Set', SET_NODE_TYPE),
|
||||
mockNode('Code', CODE_NODE_TYPE),
|
||||
mockNode('Rename', SET_NODE_TYPE),
|
||||
mockNode('Chat Trigger', CHAT_TRIGGER_NODE_TYPE),
|
||||
mockNode('Agent', AGENT_NODE_TYPE),
|
||||
mockNode('End', NO_OP_NODE_TYPE),
|
||||
];
|
||||
|
||||
export const defaultNodeTypes = mockNodes.reduce<INodeTypeData>((acc, { type }) => {
|
||||
acc[type] = mockLoadedClass(type);
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
}, {});
|
||||
|
||||
function getKnownTypes(): IDataObject {
|
||||
return {};
|
||||
}
|
||||
export const defaultNodeDescriptions = Object.values(defaultNodeTypes).map(
|
||||
({ type }) => type.description,
|
||||
) as INodeTypeDescription[];
|
||||
|
||||
function getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
function getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(getByName(nodeType), version);
|
||||
}
|
||||
|
||||
return {
|
||||
getKnownTypes,
|
||||
getByName,
|
||||
getByNameAndVersion,
|
||||
};
|
||||
}
|
||||
const nodeTypes = mock<INodeTypes>({
|
||||
getByName(nodeType) {
|
||||
return defaultNodeTypes[nodeType].type;
|
||||
},
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(defaultNodeTypes[nodeType].type, version);
|
||||
},
|
||||
});
|
||||
|
||||
export function createTestWorkflowObject({
|
||||
id = uuid(),
|
||||
|
@ -51,7 +79,6 @@ export function createTestWorkflowObject({
|
|||
nodes = [],
|
||||
connections = {},
|
||||
active = false,
|
||||
nodeTypes = {},
|
||||
staticData = {},
|
||||
settings = {},
|
||||
pinData = {},
|
||||
|
@ -61,7 +88,6 @@ export function createTestWorkflowObject({
|
|||
nodes?: INode[];
|
||||
connections?: IConnections;
|
||||
active?: boolean;
|
||||
nodeTypes?: INodeTypeData;
|
||||
staticData?: IDataObject;
|
||||
settings?: IWorkflowSettings;
|
||||
pinData?: IPinData;
|
||||
|
@ -75,38 +101,10 @@ export function createTestWorkflowObject({
|
|||
staticData,
|
||||
settings,
|
||||
pinData,
|
||||
nodeTypes: createTestNodeTypes(nodeTypes),
|
||||
nodeTypes,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTestWorkflow(options: {
|
||||
id?: string;
|
||||
name: string;
|
||||
active?: boolean;
|
||||
createdAt?: number | string;
|
||||
updatedAt?: number | string;
|
||||
nodes?: INodeUi[];
|
||||
connections?: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
tags?: ITag[] | string[];
|
||||
pinData?: IPinData;
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
homeProject?: ProjectSharingData;
|
||||
versionId?: string;
|
||||
usedCredentials?: IUsedCredential[];
|
||||
meta?: WorkflowMetadata;
|
||||
}): IWorkflowDb {
|
||||
return {
|
||||
...options,
|
||||
createdAt: options.createdAt ?? '',
|
||||
updatedAt: options.updatedAt ?? '',
|
||||
versionId: options.versionId ?? '',
|
||||
id: options.id ?? uuid(),
|
||||
active: options.active ?? false,
|
||||
connections: options.connections ?? {},
|
||||
} as IWorkflowDb;
|
||||
}
|
||||
|
||||
export function createTestNode(node: Partial<INode> = {}): INode {
|
||||
return {
|
||||
id: uuid(),
|
||||
|
@ -118,27 +116,3 @@ export function createTestNode(node: Partial<INode> = {}): INode {
|
|||
...node,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestRouteLocation({
|
||||
path = '',
|
||||
params = {},
|
||||
fullPath = path,
|
||||
hash = '',
|
||||
matched = [],
|
||||
redirectedFrom = undefined,
|
||||
name = path,
|
||||
meta = {},
|
||||
query = {},
|
||||
}: Partial<RouteLocationNormalized> = {}): RouteLocationNormalized {
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
fullPath,
|
||||
hash,
|
||||
matched,
|
||||
redirectedFrom,
|
||||
name,
|
||||
meta,
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||
import { STORES } from '@/constants';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import * as testNodeTypes from './testData/nodeTypesTestData';
|
||||
import merge from 'lodash-es/merge';
|
||||
import { groupNodeTypesByNameAndType } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||
|
||||
const defaultState = {
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
const twitterV1 = mock<INodeTypeDescription>({
|
||||
version: 1,
|
||||
credentials: [{ name: 'twitterOAuth1Api', required: true }],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||
});
|
||||
|
||||
const twitterV2 = mock<INodeTypeDescription>({
|
||||
version: 2,
|
||||
credentials: [{ name: 'twitterOAuth2Api', required: true }],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||
});
|
||||
|
||||
const nodeTypes = groupNodeTypesByNameAndType([twitterV1, twitterV2]);
|
||||
const initialState = {
|
||||
[STORES.CREDENTIALS]: {},
|
||||
[STORES.NODE_TYPES]: {},
|
||||
[STORES.NODE_TYPES]: { nodeTypes },
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(CredentialIcon, {
|
||||
pinia: createTestingPinia({
|
||||
initialState: defaultState,
|
||||
}),
|
||||
pinia: createTestingPinia({ initialState }),
|
||||
global: {
|
||||
stubs: ['n8n-tooltip'],
|
||||
},
|
||||
|
@ -25,17 +38,7 @@ describe('CredentialIcon', () => {
|
|||
|
||||
it('shows correct icon for credential type that is for the latest node type version', () => {
|
||||
const { baseElement } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: merge(defaultState, {
|
||||
[STORES.CREDENTIALS]: {},
|
||||
[STORES.NODE_TYPES]: {
|
||||
nodeTypes: groupNodeTypesByNameAndType([
|
||||
testNodeTypes.twitterV1,
|
||||
testNodeTypes.twitterV2,
|
||||
]),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
pinia: createTestingPinia({ initialState }),
|
||||
props: {
|
||||
credentialTypeName: 'twitterOAuth2Api',
|
||||
},
|
||||
|
@ -49,17 +52,7 @@ describe('CredentialIcon', () => {
|
|||
|
||||
it('shows correct icon for credential type that is for an older node type version', () => {
|
||||
const { baseElement } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: merge(defaultState, {
|
||||
[STORES.CREDENTIALS]: {},
|
||||
[STORES.NODE_TYPES]: {
|
||||
nodeTypes: groupNodeTypesByNameAndType([
|
||||
testNodeTypes.twitterV1,
|
||||
testNodeTypes.twitterV2,
|
||||
]),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
pinia: createTestingPinia({ initialState }),
|
||||
props: {
|
||||
credentialTypeName: 'twitterOAuth1Api',
|
||||
},
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import NodeDetailsView from '@/components/NodeDetailsView.vue';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { uuid } from '@jsplumb/util';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { defaultMockNodeTypesArray } from '@/__tests__/defaults';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
|
||||
async function createPiniaWithActiveNode(node: INode) {
|
||||
const workflowId = uuid();
|
||||
const workflow = createTestWorkflow({
|
||||
id: workflowId,
|
||||
name: 'Test Workflow',
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||
|
||||
async function createPiniaWithActiveNode() {
|
||||
const node = mockNodes[0];
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
connections: {},
|
||||
active: true,
|
||||
nodes: [node],
|
||||
|
@ -31,7 +30,7 @@ async function createPiniaWithActiveNode(node: INode) {
|
|||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
nodeTypesStore.setNodeTypes(defaultMockNodeTypesArray);
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
workflowsStore.workflow = workflow;
|
||||
ndvStore.activeNodeName = node.name;
|
||||
|
||||
|
@ -72,12 +71,7 @@ describe('NodeDetailsView', () => {
|
|||
|
||||
it('should render correctly', async () => {
|
||||
const wrapper = renderComponent({
|
||||
pinia: await createPiniaWithActiveNode(
|
||||
createTestNode({
|
||||
name: 'Manual Trigger',
|
||||
type: 'manualTrigger',
|
||||
}),
|
||||
),
|
||||
pinia: await createPiniaWithActiveNode(),
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
import WorkflowLMChatModal from '@/components/WorkflowLMChat.vue';
|
||||
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { uuid } from '@jsplumb/util';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { IConnections, INode } from 'n8n-workflow';
|
||||
|
||||
import WorkflowLMChatModal from '@/components/WorkflowLMChat.vue';
|
||||
import { WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { testingNodeTypes, mockNodeTypesToArray } from '@/__tests__/defaults';
|
||||
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { IConnections } from 'n8n-workflow';
|
||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
|
||||
props: {
|
||||
teleported: false,
|
||||
appendToBody: false,
|
||||
},
|
||||
});
|
||||
|
||||
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
|
||||
const { withConnections, withAgentNode } = options;
|
||||
const workflowId = uuid();
|
||||
const connections: IConnections = {
|
||||
'Chat Trigger': {
|
||||
main: [
|
||||
|
@ -39,25 +31,23 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
|||
},
|
||||
};
|
||||
|
||||
const workflow = createTestWorkflow({
|
||||
id: workflowId,
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
|
||||
props: {
|
||||
teleported: false,
|
||||
appendToBody: false,
|
||||
},
|
||||
});
|
||||
|
||||
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
|
||||
const { withConnections, withAgentNode } = options;
|
||||
|
||||
const chatTriggerNode = mockNodes[4];
|
||||
const agentNode = mockNodes[5];
|
||||
const nodes: INode[] = [chatTriggerNode];
|
||||
if (withAgentNode) nodes.push(agentNode);
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes,
|
||||
...(withConnections ? { connections } : {}),
|
||||
nodes: [
|
||||
createTestNode({
|
||||
name: 'Chat Trigger',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
}),
|
||||
...(withAgentNode
|
||||
? [
|
||||
createTestNode({
|
||||
name: 'Agent',
|
||||
type: AGENT_NODE_TYPE,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
const pinia = createPinia();
|
||||
|
@ -67,12 +57,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
|||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
nodeTypesStore.setNodeTypes(
|
||||
mockNodeTypesToArray({
|
||||
[CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[CHAT_TRIGGER_NODE_TYPE],
|
||||
[AGENT_NODE_TYPE]: testingNodeTypes[AGENT_NODE_TYPE],
|
||||
}),
|
||||
);
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
workflowsStore.workflow = workflow;
|
||||
|
||||
await useSettingsStore().getSettings();
|
||||
|
|
|
@ -1,964 +0,0 @@
|
|||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
export const twitterV2: INodeTypeDescription = {
|
||||
displayName: 'X (Formerly Twitter)',
|
||||
name: 'n8n-nodes-base.twitter',
|
||||
group: ['output'],
|
||||
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||
description: 'Post, like, and search tweets, send messages, search users, and add users to lists',
|
||||
defaultVersion: 2,
|
||||
version: 2,
|
||||
defaults: { name: 'X' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [{ name: 'twitterOAuth2Api', required: true }],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Direct Message',
|
||||
value: 'directMessage',
|
||||
description: 'Send a direct message to a user',
|
||||
},
|
||||
{ name: 'List', value: 'list', description: 'Add a user to a list' },
|
||||
{ name: 'Tweet', value: 'tweet', description: 'Create, like, search, or delete a tweet' },
|
||||
{ name: 'User', value: 'user', description: 'Search users by username' },
|
||||
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||
],
|
||||
default: 'tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['directMessage'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Send a direct message to a user',
|
||||
action: 'Create Direct Message',
|
||||
},
|
||||
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'username', value: '' },
|
||||
required: true,
|
||||
description: 'The user you want to send the message to',
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. n8n',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1068479892537384960',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
typeOptions: { rows: 2 },
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
description:
|
||||
'The text of the direct message. URL encoding is required. Max length of 10,000 characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachment ID',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '1664279886239010824',
|
||||
description: 'The attachment ID to associate with the message',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['list'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Add Member',
|
||||
value: 'add',
|
||||
description: 'Add a member to a list',
|
||||
action: 'Add Member to List',
|
||||
},
|
||||
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||
],
|
||||
default: 'add',
|
||||
},
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
required: true,
|
||||
description: 'The list you want to add the user to',
|
||||
displayOptions: { show: { operation: ['add'], resource: ['list'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 99923132',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/i/lists/99923132',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'username', value: '' },
|
||||
required: true,
|
||||
description: 'The user you want to add to the list',
|
||||
displayOptions: { show: { operation: ['add'], resource: ['list'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. n8n',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1068479892537384960',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create, quote, or reply to a tweet',
|
||||
action: 'Create Tweet',
|
||||
},
|
||||
{ name: 'Delete', value: 'delete', description: 'Delete a tweet', action: 'Delete Tweet' },
|
||||
{ name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like Tweet' },
|
||||
{
|
||||
name: 'Retweet',
|
||||
value: 'retweet',
|
||||
description: 'Retweet a tweet',
|
||||
action: 'Retweet Tweet',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search for tweets from the last seven days',
|
||||
action: 'Search Tweets',
|
||||
},
|
||||
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 2 },
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||
description:
|
||||
'The text of the status update. URLs must be encoded. Links wrapped with the t.co shortener will affect character count',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Location ID',
|
||||
name: 'location',
|
||||
type: 'string',
|
||||
placeholder: '4e696bef7e24d378',
|
||||
default: '',
|
||||
description: 'Location information for the tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Media ID',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '1664279886239010824',
|
||||
description: 'The attachment ID to associate with the message',
|
||||
},
|
||||
{
|
||||
displayName: 'Quote a Tweet',
|
||||
name: 'inQuoteToStatusId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
description: 'The tweet being quoted',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Reply to Tweet',
|
||||
name: 'inReplyToStatusId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
description: 'The tweet being replied to',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Locations are not supported due to Twitter V2 API limitations',
|
||||
name: 'noticeLocation',
|
||||
type: 'notice',
|
||||
displayOptions: { show: { '/additionalFields.location': [''] } },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachements are not supported due to Twitter V2 API limitations',
|
||||
name: 'noticeAttachments',
|
||||
type: 'notice',
|
||||
displayOptions: { show: { '/additionalFields.attachments': [''] } },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet',
|
||||
name: 'tweetDeleteId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
required: true,
|
||||
description: 'The tweet to delete',
|
||||
displayOptions: { show: { resource: ['tweet'], operation: ['delete'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet',
|
||||
name: 'tweetId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
required: true,
|
||||
description: 'The tweet to like',
|
||||
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Search Term',
|
||||
name: 'searchText',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
placeholder: 'e.g. automation',
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||
description:
|
||||
'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
displayOptions: { show: { resource: ['tweet'], operation: ['search'] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
typeOptions: { minValue: 1 },
|
||||
displayOptions: { show: { resource: ['tweet'], operation: ['search'], returnAll: [false] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Sort Order',
|
||||
name: 'sortOrder',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Recent', value: 'recency' },
|
||||
{ name: 'Relevant', value: 'relevancy' },
|
||||
],
|
||||
description: 'The order in which to return results',
|
||||
default: 'recency',
|
||||
},
|
||||
{
|
||||
displayName: 'After',
|
||||
name: 'startTime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description:
|
||||
"Tweets before this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
|
||||
},
|
||||
{
|
||||
displayName: 'Before',
|
||||
name: 'endTime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description:
|
||||
"Tweets after this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet Fields',
|
||||
name: 'tweetFieldsObject',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{ name: 'Attachments', value: 'attachments' },
|
||||
{ name: 'Author ID', value: 'author_id' },
|
||||
{ name: 'Context Annotations', value: 'context_annotations' },
|
||||
{ name: 'Conversation ID', value: 'conversation_id' },
|
||||
{ name: 'Created At', value: 'created_at' },
|
||||
{ name: 'Edit Controls', value: 'edit_controls' },
|
||||
{ name: 'Entities', value: 'entities' },
|
||||
{ name: 'Geo', value: 'geo' },
|
||||
{ name: 'ID', value: 'id' },
|
||||
{ name: 'In Reply To User ID', value: 'in_reply_to_user_id' },
|
||||
{ name: 'Lang', value: 'lang' },
|
||||
{ name: 'Non Public Metrics', value: 'non_public_metrics' },
|
||||
{ name: 'Public Metrics', value: 'public_metrics' },
|
||||
{ name: 'Organic Metrics', value: 'organic_metrics' },
|
||||
{ name: 'Promoted Metrics', value: 'promoted_metrics' },
|
||||
{ name: 'Possibly Sensitive', value: 'possibly_sensitive' },
|
||||
{ name: 'Referenced Tweets', value: 'referenced_tweets' },
|
||||
{ name: 'Reply Settings', value: 'reply_settings' },
|
||||
{ name: 'Source', value: 'source' },
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'Withheld', value: 'withheld' },
|
||||
],
|
||||
default: [],
|
||||
description:
|
||||
'The fields to add to each returned tweet object. Default fields are: ID, text, edit_history_tweet_ids.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet',
|
||||
name: 'tweetId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'id', value: '' },
|
||||
required: true,
|
||||
description: 'The tweet to retweet',
|
||||
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['user'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'searchUser',
|
||||
description: 'Retrieve a user by username',
|
||||
action: 'Get User',
|
||||
},
|
||||
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||
],
|
||||
default: 'searchUser',
|
||||
},
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'username', value: '' },
|
||||
required: true,
|
||||
description: 'The user you want to search',
|
||||
displayOptions: {
|
||||
show: { operation: ['searchUser'], resource: ['user'] },
|
||||
hide: { me: [true] },
|
||||
},
|
||||
modes: [
|
||||
{
|
||||
displayName: 'By Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. n8n',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [],
|
||||
placeholder: 'e.g. 1068479892537384960',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Me',
|
||||
name: 'me',
|
||||
type: 'boolean',
|
||||
displayOptions: { show: { operation: ['searchUser'], resource: ['user'] } },
|
||||
default: false,
|
||||
description: 'Whether you want to search the authenticated user',
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||
codex: {
|
||||
categories: ['Marketing & Content'],
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{ url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twitter/' },
|
||||
],
|
||||
credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/twitter' }],
|
||||
},
|
||||
alias: ['Tweet', 'Twitter', 'X', 'X API'],
|
||||
},
|
||||
};
|
||||
|
||||
export const twitterV1: INodeTypeDescription = {
|
||||
displayName: 'X (Formerly Twitter)',
|
||||
name: 'n8n-nodes-base.twitter',
|
||||
group: ['output'],
|
||||
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||
description: 'Consume Twitter API',
|
||||
defaultVersion: 2,
|
||||
version: 1,
|
||||
defaults: { name: 'Twitter' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [{ name: 'twitterOAuth1Api', required: true }],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{ name: 'Direct Message', value: 'directMessage' },
|
||||
{ name: 'Tweet', value: 'tweet' },
|
||||
],
|
||||
default: 'tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['directMessage'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a direct message',
|
||||
action: 'Create a direct message',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'userId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
description: 'The ID of the user who should receive the direct message',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
description:
|
||||
'The text of your Direct Message. URL encode as necessary. Max length of 10,000 characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachment',
|
||||
name: 'attachment',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
description:
|
||||
'Name of the binary property which contain data that should be added to the direct message as attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create or reply a tweet',
|
||||
action: 'Create a tweet',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a tweet',
|
||||
action: 'Delete a tweet',
|
||||
},
|
||||
{ name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like a tweet' },
|
||||
{
|
||||
name: 'Retweet',
|
||||
value: 'retweet',
|
||||
description: 'Retweet a tweet',
|
||||
action: 'Retweet a tweet',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search tweets',
|
||||
action: 'Search for tweets',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||
description:
|
||||
'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
description:
|
||||
'Name of the binary properties which contain data which should be added to tweet as attachment. Multiple ones can be comma-separated.',
|
||||
},
|
||||
{
|
||||
displayName: 'Display Coordinates',
|
||||
name: 'displayCoordinates',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether or not to put a pin on the exact coordinates a Tweet has been sent from',
|
||||
},
|
||||
{
|
||||
displayName: 'In Reply to Tweet',
|
||||
name: 'inReplyToStatusId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The ID of an existing status that the update is in reply to',
|
||||
},
|
||||
{
|
||||
displayName: 'Location',
|
||||
name: 'locationFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Location',
|
||||
default: {},
|
||||
description: 'Subscriber location information.n',
|
||||
options: [
|
||||
{
|
||||
name: 'locationFieldsValues',
|
||||
displayName: 'Location',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Latitude',
|
||||
name: 'latitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location latitude',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Longitude',
|
||||
name: 'longitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location longitude',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Possibly Sensitive',
|
||||
name: 'possiblySensitive',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether you are uploading Tweet media that might be considered sensitive content such as nudity, or medical procedures',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet ID',
|
||||
name: 'tweetId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['delete'], resource: ['tweet'] } },
|
||||
description: 'The ID of the tweet to delete',
|
||||
},
|
||||
{
|
||||
displayName: 'Search Text',
|
||||
name: 'searchText',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||
description:
|
||||
'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'], returnAll: [false] } },
|
||||
typeOptions: { minValue: 1 },
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Entities',
|
||||
name: 'includeEntities',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the entities node will be included',
|
||||
},
|
||||
{
|
||||
displayName: 'Language Name or ID',
|
||||
name: 'lang',
|
||||
type: 'options',
|
||||
typeOptions: { loadOptionsMethod: 'getLanguages' },
|
||||
default: '',
|
||||
description:
|
||||
'Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is best-effort. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Location',
|
||||
name: 'locationFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Location',
|
||||
default: {},
|
||||
description: 'Subscriber location information.n',
|
||||
options: [
|
||||
{
|
||||
name: 'locationFieldsValues',
|
||||
displayName: 'Location',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Latitude',
|
||||
name: 'latitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location latitude',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Longitude',
|
||||
name: 'longitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location longitude',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Radius',
|
||||
name: 'radius',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Milles', value: 'mi' },
|
||||
{ name: 'Kilometers', value: 'km' },
|
||||
],
|
||||
required: true,
|
||||
description:
|
||||
'Returns tweets by users located within a given radius of the given latitude/longitude',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Distance',
|
||||
name: 'distance',
|
||||
type: 'number',
|
||||
typeOptions: { minValue: 0 },
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Result Type',
|
||||
name: 'resultType',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Mixed',
|
||||
value: 'mixed',
|
||||
description: 'Include both popular and real time results in the response',
|
||||
},
|
||||
{
|
||||
name: 'Recent',
|
||||
value: 'recent',
|
||||
description: 'Return only the most recent results in the response',
|
||||
},
|
||||
{
|
||||
name: 'Popular',
|
||||
value: 'popular',
|
||||
description: 'Return only the most popular results in the response',
|
||||
},
|
||||
],
|
||||
default: 'mixed',
|
||||
description: 'Specifies what type of search results you would prefer to receive',
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet Mode',
|
||||
name: 'tweetMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Compatibility', value: 'compat' },
|
||||
{ name: 'Extended', value: 'extended' },
|
||||
],
|
||||
default: 'compat',
|
||||
description:
|
||||
'When the extended mode is selected, the response contains the entire untruncated text of the Tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Until',
|
||||
name: 'until',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Returns tweets created before the given date',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet ID',
|
||||
name: 'tweetId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||
description: 'The ID of the tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Entities',
|
||||
name: 'includeEntities',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the entities will be omitted',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tweet ID',
|
||||
name: 'tweetId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||
description: 'The ID of the tweet',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Trim User',
|
||||
name: 'trimUser',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether each tweet returned in a timeline will include a user object including only the status authors numerical ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||
};
|
|
@ -1,13 +1,12 @@
|
|||
import { computed } from 'vue';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { useActiveNode } from '@/composables/useActiveNode';
|
||||
import { useNodeType } from '@/composables/useNodeType';
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { computed } from 'vue';
|
||||
import { defaultMockNodeTypes } from '@/__tests__/defaults';
|
||||
|
||||
const node = computed(() => createTestNode({ name: 'Node', type: MANUAL_TRIGGER_NODE_TYPE }));
|
||||
const nodeType = computed(() => defaultMockNodeTypes[MANUAL_TRIGGER_NODE_TYPE]);
|
||||
const node = computed(() => mock());
|
||||
const nodeType = computed(() => mock());
|
||||
|
||||
vi.mock('@/stores/ndv.store', () => ({
|
||||
useNDVStore: vi.fn(() => ({
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
import { createTestNode, createTestWorkflow, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import type { IConnections, Workflow } from 'n8n-workflow';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { createTestWorkflowObject, mockNodes } from '@/__tests__/mocks';
|
||||
|
||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
|
@ -30,11 +32,8 @@ afterEach(() => {
|
|||
|
||||
describe('useCanvasMapping', () => {
|
||||
it('should initialize with default props', () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes: [],
|
||||
connections: {},
|
||||
});
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
|
||||
|
@ -49,14 +48,9 @@ describe('useCanvasMapping', () => {
|
|||
|
||||
describe('elements', () => {
|
||||
it('should map nodes to canvas elements', () => {
|
||||
const node = createTestNode({
|
||||
name: 'Node',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
const workflow = createTestWorkflow({
|
||||
name: 'Test Workflow',
|
||||
nodes: [node],
|
||||
connections: {},
|
||||
const manualTriggerNode = mockNodes[0];
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes: [manualTriggerNode],
|
||||
});
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
|
||||
|
@ -67,14 +61,14 @@ describe('useCanvasMapping', () => {
|
|||
|
||||
expect(elements.value).toEqual([
|
||||
{
|
||||
id: node.id,
|
||||
label: node.name,
|
||||
id: manualTriggerNode.id,
|
||||
label: manualTriggerNode.name,
|
||||
type: 'canvas-node',
|
||||
position: { x: 0, y: 0 },
|
||||
position: expect.anything(),
|
||||
data: {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
typeVersion: 1,
|
||||
id: manualTriggerNode.id,
|
||||
type: manualTriggerNode.type,
|
||||
typeVersion: expect.anything(),
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
renderType: 'default',
|
||||
|
@ -86,24 +80,16 @@ describe('useCanvasMapping', () => {
|
|||
|
||||
describe('connections', () => {
|
||||
it('should map connections to canvas connections', () => {
|
||||
const nodeA = createTestNode({
|
||||
name: 'Node A',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
const nodeB = createTestNode({
|
||||
name: 'Node B',
|
||||
type: SET_NODE_TYPE,
|
||||
});
|
||||
const workflow = createTestWorkflow({
|
||||
name: 'Test Workflow',
|
||||
nodes: [nodeA, nodeB],
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes: [manualTriggerNode, setNode],
|
||||
connections: {
|
||||
[nodeA.name]: {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionType.Main]: [
|
||||
[{ node: nodeB.name, type: NodeConnectionType.Main, index: 0 }],
|
||||
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
} as IConnections,
|
||||
},
|
||||
});
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
|
||||
|
@ -115,7 +101,7 @@ describe('useCanvasMapping', () => {
|
|||
expect(connections.value).toEqual([
|
||||
{
|
||||
data: {
|
||||
fromNodeName: nodeA.name,
|
||||
fromNodeName: manualTriggerNode.name,
|
||||
source: {
|
||||
index: 0,
|
||||
type: NodeConnectionType.Main,
|
||||
|
@ -125,11 +111,11 @@ describe('useCanvasMapping', () => {
|
|||
type: NodeConnectionType.Main,
|
||||
},
|
||||
},
|
||||
id: `[${nodeA.id}/${NodeConnectionType.Main}/0][${nodeB.id}/${NodeConnectionType.Main}/0]`,
|
||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.Main}/0][${setNode.id}/${NodeConnectionType.Main}/0]`,
|
||||
label: '',
|
||||
source: nodeA.id,
|
||||
source: manualTriggerNode.id,
|
||||
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
|
||||
target: nodeB.id,
|
||||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
||||
type: 'canvas-edge',
|
||||
},
|
||||
|
@ -137,24 +123,16 @@ describe('useCanvasMapping', () => {
|
|||
});
|
||||
|
||||
it('should map multiple input types to canvas connections', () => {
|
||||
const nodeA = createTestNode({
|
||||
name: 'Node A',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
const nodeB = createTestNode({
|
||||
name: 'Node B',
|
||||
type: SET_NODE_TYPE,
|
||||
});
|
||||
const workflow = createTestWorkflow({
|
||||
name: 'Test Workflow',
|
||||
nodes: [nodeA, nodeB],
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes: [manualTriggerNode, setNode],
|
||||
connections: {
|
||||
'Node A': {
|
||||
[manualTriggerNode.name]: {
|
||||
[NodeConnectionType.AiTool]: [
|
||||
[{ node: nodeB.name, type: NodeConnectionType.AiTool, index: 0 }],
|
||||
[{ node: setNode.name, type: NodeConnectionType.AiTool, index: 0 }],
|
||||
],
|
||||
[NodeConnectionType.AiDocument]: [
|
||||
[{ node: nodeB.name, type: NodeConnectionType.AiDocument, index: 1 }],
|
||||
[{ node: setNode.name, type: NodeConnectionType.AiDocument, index: 1 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -169,7 +147,7 @@ describe('useCanvasMapping', () => {
|
|||
expect(connections.value).toEqual([
|
||||
{
|
||||
data: {
|
||||
fromNodeName: nodeA.name,
|
||||
fromNodeName: manualTriggerNode.name,
|
||||
source: {
|
||||
index: 0,
|
||||
type: NodeConnectionType.AiTool,
|
||||
|
@ -179,17 +157,17 @@ describe('useCanvasMapping', () => {
|
|||
type: NodeConnectionType.AiTool,
|
||||
},
|
||||
},
|
||||
id: `[${nodeA.id}/${NodeConnectionType.AiTool}/0][${nodeB.id}/${NodeConnectionType.AiTool}/0]`,
|
||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.AiTool}/0][${setNode.id}/${NodeConnectionType.AiTool}/0]`,
|
||||
label: '',
|
||||
source: nodeA.id,
|
||||
source: manualTriggerNode.id,
|
||||
sourceHandle: `outputs/${NodeConnectionType.AiTool}/0`,
|
||||
target: nodeB.id,
|
||||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.AiTool}/0`,
|
||||
type: 'canvas-edge',
|
||||
},
|
||||
{
|
||||
data: {
|
||||
fromNodeName: nodeA.name,
|
||||
fromNodeName: manualTriggerNode.name,
|
||||
source: {
|
||||
index: 0,
|
||||
type: NodeConnectionType.AiDocument,
|
||||
|
@ -199,11 +177,11 @@ describe('useCanvasMapping', () => {
|
|||
type: NodeConnectionType.AiDocument,
|
||||
},
|
||||
},
|
||||
id: `[${nodeA.id}/${NodeConnectionType.AiDocument}/0][${nodeB.id}/${NodeConnectionType.AiDocument}/1]`,
|
||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.AiDocument}/0][${setNode.id}/${NodeConnectionType.AiDocument}/1]`,
|
||||
label: '',
|
||||
source: nodeA.id,
|
||||
source: manualTriggerNode.id,
|
||||
sourceHandle: `outputs/${NodeConnectionType.AiDocument}/0`,
|
||||
target: nodeB.id,
|
||||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.AiDocument}/1`,
|
||||
type: 'canvas-edge',
|
||||
},
|
|
@ -1,3 +1,8 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import type { IConnection } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import type { CanvasElement } from '@/types';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
@ -5,12 +10,8 @@ import { RemoveNodeCommand } from '@/models/history';
|
|||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useHistoryStore } from '@/stores/history.store';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import type { IConnection } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
|
||||
describe('useCanvasOperations', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
|
@ -255,23 +256,6 @@ describe('useCanvasOperations', () => {
|
|||
expect(uiStore.stateIsDirty).toBe(false);
|
||||
});
|
||||
|
||||
// @TODO Implement once the isConnectionAllowed method is implemented
|
||||
it.skip('should not create a connection if connection is not allowed', () => {
|
||||
const addConnectionSpy = vi
|
||||
.spyOn(workflowsStore, 'addConnection')
|
||||
.mockImplementation(() => {});
|
||||
const connection: Connection = { source: 'sourceNode', target: 'targetNode' };
|
||||
|
||||
vi.spyOn(workflowsStore, 'getNodeById')
|
||||
.mockReturnValueOnce(createTestNode())
|
||||
.mockReturnValueOnce(createTestNode());
|
||||
|
||||
canvasOperations.createConnection(connection);
|
||||
|
||||
expect(addConnectionSpy).not.toHaveBeenCalled();
|
||||
expect(uiStore.stateIsDirty).toBe(false);
|
||||
});
|
||||
|
||||
it('should create a connection if source and target nodes exist and connection is allowed', () => {
|
||||
const addConnectionSpy = vi
|
||||
.spyOn(workflowsStore, 'addConnection')
|
|
@ -1,43 +1,39 @@
|
|||
import { useNodeBase } from '@/composables/useNodeBase';
|
||||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||
import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { mock, mockClear } from 'vitest-mock-extended';
|
||||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||
import type { INode, INodeTypeDescription, Workflow } from 'n8n-workflow';
|
||||
|
||||
import { useNodeBase } from '@/composables/useNodeBase';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { findNodeTypeDescriptionByName } from '@/__tests__/defaults';
|
||||
import { SET_NODE_TYPE } from '@/constants';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { INodeTypeDescription, Workflow } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { Mock } from '@vitest/spy';
|
||||
|
||||
describe('useNodeBase', () => {
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let instance: Record<string, Mock>;
|
||||
let workflowObject: Workflow;
|
||||
let emit: (event: string, ...args: unknown[]) => void;
|
||||
let node: INodeUi;
|
||||
let nodeTypeDescription: INodeTypeDescription;
|
||||
let nodeBase: ReturnType<typeof useNodeBase>;
|
||||
|
||||
const jsPlumbInstance = mock<BrowserJsPlumbInstance>();
|
||||
const nodeTypeDescription = mock<INodeTypeDescription>({
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
});
|
||||
const workflowObject = mock<Workflow>();
|
||||
const node = mock<INode>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockClear(jsPlumbInstance);
|
||||
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
uiStore = useUIStore();
|
||||
|
||||
instance = {
|
||||
addEndpoint: vi.fn().mockReturnValue({}),
|
||||
};
|
||||
workflowObject = createTestWorkflowObject({ nodes: [], connections: {} });
|
||||
node = createTestNode();
|
||||
nodeTypeDescription = findNodeTypeDescriptionByName(SET_NODE_TYPE);
|
||||
emit = vi.fn();
|
||||
|
||||
nodeBase = useNodeBase({
|
||||
instance: instance as unknown as BrowserJsPlumbInstance,
|
||||
instance: jsPlumbInstance,
|
||||
name: node.name,
|
||||
workflowObject,
|
||||
isReadOnly: false,
|
||||
|
@ -54,83 +50,89 @@ describe('useNodeBase', () => {
|
|||
|
||||
describe('addInputEndpoints', () => {
|
||||
it('should add input endpoints correctly', () => {
|
||||
const { addInputEndpoints } = nodeBase;
|
||||
|
||||
jsPlumbInstance.addEndpoint.mockReturnValue(mock());
|
||||
vi.spyOn(workflowsStore, 'getNodeByName').mockReturnValue(node);
|
||||
|
||||
addInputEndpoints(node, nodeTypeDescription);
|
||||
|
||||
const addEndpointCall = instance.addEndpoint.mock.calls[0][1];
|
||||
nodeBase.addInputEndpoints(node, nodeTypeDescription);
|
||||
|
||||
expect(workflowsStore.getNodeByName).toHaveBeenCalledWith(node.name);
|
||||
expect(addEndpointCall.anchor).toEqual([0.01, 0.5, -1, 0]);
|
||||
expect(addEndpointCall.cssClass).toEqual('rect-input-endpoint');
|
||||
expect(addEndpointCall.dragAllowedWhenFull).toEqual(true);
|
||||
expect(addEndpointCall.enabled).toEqual(true);
|
||||
expect(addEndpointCall.endpoint).toEqual('Rectangle');
|
||||
expect(addEndpointCall.hoverClass).toEqual('rect-input-endpoint-hover');
|
||||
expect(addEndpointCall.hoverPaintStyle).toMatchObject({
|
||||
fill: 'var(--color-primary)',
|
||||
height: 20,
|
||||
lineWidth: 0,
|
||||
stroke: 'var(--color-primary)',
|
||||
expect(jsPlumbInstance.addEndpoint).toHaveBeenCalledWith(undefined, {
|
||||
anchor: [0.01, 0.5, -1, 0],
|
||||
maxConnections: -1,
|
||||
endpoint: 'Rectangle',
|
||||
paintStyle: {
|
||||
width: 8,
|
||||
});
|
||||
expect(addEndpointCall.maxConnections).toEqual(-1);
|
||||
expect(addEndpointCall.paintStyle).toMatchObject({
|
||||
height: 20,
|
||||
fill: 'var(--node-type-main-color)',
|
||||
height: 20,
|
||||
lineWidth: 0,
|
||||
stroke: 'var(--node-type-main-color)',
|
||||
lineWidth: 0,
|
||||
},
|
||||
hoverPaintStyle: {
|
||||
width: 8,
|
||||
});
|
||||
expect(addEndpointCall.parameters).toMatchObject({
|
||||
height: 20,
|
||||
fill: 'var(--color-primary)',
|
||||
stroke: 'var(--color-primary)',
|
||||
lineWidth: 0,
|
||||
},
|
||||
source: false,
|
||||
target: false,
|
||||
parameters: {
|
||||
connection: 'target',
|
||||
index: 0,
|
||||
nodeId: node.id,
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
enabled: true,
|
||||
cssClass: 'rect-input-endpoint',
|
||||
dragAllowedWhenFull: true,
|
||||
hoverClass: 'rect-input-endpoint-hover',
|
||||
uuid: `${node.id}-input0`,
|
||||
});
|
||||
expect(addEndpointCall.scope).toBeUndefined();
|
||||
expect(addEndpointCall.source).toBeFalsy();
|
||||
expect(addEndpointCall.target).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addOutputEndpoints', () => {
|
||||
it('should add output endpoints correctly', () => {
|
||||
const { addOutputEndpoints } = nodeBase;
|
||||
const getNodeByNameSpy = vi.spyOn(workflowsStore, 'getNodeByName').mockReturnValue(node);
|
||||
|
||||
addOutputEndpoints(node, nodeTypeDescription);
|
||||
|
||||
const addEndpointCall = instance.addEndpoint.mock.calls[0][1];
|
||||
nodeBase.addOutputEndpoints(node, nodeTypeDescription);
|
||||
|
||||
expect(getNodeByNameSpy).toHaveBeenCalledWith(node.name);
|
||||
expect(addEndpointCall.anchor).toEqual([0.99, 0.5, 1, 0]);
|
||||
expect(addEndpointCall.cssClass).toEqual('dot-output-endpoint');
|
||||
expect(addEndpointCall.dragAllowedWhenFull).toEqual(false);
|
||||
expect(addEndpointCall.enabled).toEqual(true);
|
||||
expect(addEndpointCall.endpoint).toEqual({
|
||||
expect(jsPlumbInstance.addEndpoint).toHaveBeenCalledWith(undefined, {
|
||||
anchor: [0.99, 0.5, 1, 0],
|
||||
connectionsDirected: true,
|
||||
cssClass: 'dot-output-endpoint',
|
||||
dragAllowedWhenFull: false,
|
||||
enabled: true,
|
||||
endpoint: {
|
||||
options: {
|
||||
radius: 9,
|
||||
},
|
||||
type: 'Dot',
|
||||
});
|
||||
expect(addEndpointCall.hoverClass).toEqual('dot-output-endpoint-hover');
|
||||
expect(addEndpointCall.hoverPaintStyle).toMatchObject({
|
||||
},
|
||||
hoverClass: 'dot-output-endpoint-hover',
|
||||
hoverPaintStyle: {
|
||||
fill: 'var(--color-primary)',
|
||||
});
|
||||
expect(addEndpointCall.maxConnections).toEqual(-1);
|
||||
expect(addEndpointCall.paintStyle).toMatchObject({
|
||||
outlineStroke: 'none',
|
||||
strokeWidth: 9,
|
||||
},
|
||||
maxConnections: -1,
|
||||
paintStyle: {
|
||||
fill: 'var(--node-type-main-color)',
|
||||
});
|
||||
expect(addEndpointCall.parameters).toMatchObject({
|
||||
outlineStroke: 'none',
|
||||
strokeWidth: 9,
|
||||
},
|
||||
parameters: {
|
||||
connection: 'source',
|
||||
index: 0,
|
||||
nodeId: node.id,
|
||||
type: 'main',
|
||||
},
|
||||
scope: undefined,
|
||||
source: true,
|
||||
target: false,
|
||||
uuid: `${node.id}-output0`,
|
||||
});
|
||||
expect(addEndpointCall.scope).toBeUndefined();
|
||||
expect(addEndpointCall.source).toBeTruthy();
|
||||
expect(addEndpointCall.target).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type router from 'vue-router';
|
||||
import { ExpressionError, type IPinData, type IRunData, type Workflow } from 'n8n-workflow';
|
||||
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import type { IStartRunData, IWorkflowData } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ExpressionError, type IPinData, type IRunData, type Workflow } from 'n8n-workflow';
|
||||
import type * as router from 'vue-router';
|
||||
|
||||
vi.mock('@/stores/workflows.store', () => ({
|
||||
useWorkflowsStore: vi.fn().mockReturnValue({
|
|
@ -231,7 +231,7 @@ export function useNodeBase({
|
|||
const endpoint = instance?.addEndpoint(
|
||||
refs.value[data.value?.name ?? ''] as Element,
|
||||
newEndpointData,
|
||||
) as Endpoint;
|
||||
);
|
||||
addEndpointTestingData(endpoint, 'input', typeIndex);
|
||||
if (inputConfiguration.displayName ?? nodeTypeData.inputNames?.[i]) {
|
||||
// Apply input names if they got set
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
natives,
|
||||
} from '@/plugins/codemirror/completions/datatype.completions';
|
||||
|
||||
import { mockNodes, mockProxy } from './mock';
|
||||
import { mockProxy } from './mock';
|
||||
import type { CompletionSource, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { CompletionContext } from '@codemirror/autocomplete';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
@ -30,6 +30,7 @@ import {
|
|||
STRING_RECOMMENDED_OPTIONS,
|
||||
} from '../constants';
|
||||
import { set, uniqBy } from 'lodash-es';
|
||||
import { mockNodes } from '@/__tests__/mocks';
|
||||
|
||||
let externalSecretsStore: ReturnType<typeof useExternalSecretsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
|
|
@ -1,126 +1,7 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type {
|
||||
INode,
|
||||
IConnections,
|
||||
IRunExecutionData,
|
||||
IExecuteData,
|
||||
INodeTypeData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { INode, IRunExecutionData, IExecuteData } from 'n8n-workflow';
|
||||
import { WorkflowDataProxy } from 'n8n-workflow';
|
||||
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
|
||||
const nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'n8n-nodes-base.set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
type: 'n8n-nodes-base.set',
|
||||
parameters: {
|
||||
functionCode:
|
||||
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [280, 200],
|
||||
},
|
||||
{
|
||||
name: 'Rename',
|
||||
type: 'n8n-nodes-base.set',
|
||||
parameters: {
|
||||
value1: 'data',
|
||||
value2: 'initialName',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [460, 200],
|
||||
},
|
||||
{
|
||||
name: 'End',
|
||||
type: 'n8n-nodes-base.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [640, 200],
|
||||
},
|
||||
];
|
||||
|
||||
const connections: IConnections = {
|
||||
Start: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Function',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Function: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Rename',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Rename: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'End',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
import { createTestWorkflowObject, mockNodes } from '@/__tests__/mocks';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
resultData: {
|
||||
|
@ -265,20 +146,19 @@ const runExecutionData: IRunExecutionData = {
|
|||
const workflow = createTestWorkflowObject({
|
||||
id: '123',
|
||||
name: 'test workflow',
|
||||
nodes,
|
||||
connections,
|
||||
nodes: mockNodes,
|
||||
connections: mock(),
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const lastNodeName = 'End';
|
||||
const lastNodeName = mockNodes[mockNodes.length - 1].name;
|
||||
|
||||
const lastNodeConnectionInputData =
|
||||
runExecutionData.resultData.runData[lastNodeName][0].data!.main[0];
|
||||
|
||||
const executeData: IExecuteData = {
|
||||
data: runExecutionData.resultData.runData[lastNodeName][0].data!,
|
||||
node: nodes.find((node) => node.name === lastNodeName) as INode,
|
||||
node: mockNodes.find((node) => node.name === lastNodeName) as INode,
|
||||
source: {
|
||||
main: runExecutionData.resultData.runData[lastNodeName][0].source,
|
||||
},
|
||||
|
@ -298,20 +178,3 @@ const dataProxy = new WorkflowDataProxy(
|
|||
);
|
||||
|
||||
export const mockProxy = dataProxy.getDataProxy();
|
||||
|
||||
export const mockNodes = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: 'Manual',
|
||||
position: [0, 0],
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: 'Set',
|
||||
position: [0, 0],
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { mock } from 'vitest-mock-extended';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import {
|
||||
inferProjectIdFromRoute,
|
||||
inferResourceTypeFromRoute,
|
||||
inferResourceIdFromRoute,
|
||||
} from '../rbacUtils';
|
||||
import { createTestRouteLocation } from '@/__tests__/mocks';
|
||||
} from '@/utils/rbacUtils';
|
||||
|
||||
describe('rbacUtils', () => {
|
||||
describe('inferProjectIdFromRoute()', () => {
|
||||
it('should infer project ID from route correctly', () => {
|
||||
const route = createTestRouteLocation({ path: '/dashboard/projects/123/settings' });
|
||||
const route = mock<RouteLocationNormalized>({ path: '/dashboard/projects/123/settings' });
|
||||
const projectId = inferProjectIdFromRoute(route);
|
||||
expect(projectId).toBe('123');
|
||||
});
|
||||
|
||||
it('should return undefined for project ID if not found', () => {
|
||||
const route = createTestRouteLocation({ path: '/dashboard/settings' });
|
||||
const route = mock<RouteLocationNormalized>({ path: '/dashboard/settings' });
|
||||
const projectId = inferProjectIdFromRoute(route);
|
||||
expect(projectId).toBeUndefined();
|
||||
});
|
||||
|
@ -31,13 +32,13 @@ describe('rbacUtils', () => {
|
|||
['/source-control', 'sourceControl'],
|
||||
['/external-secrets', 'externalSecret'],
|
||||
])('should infer resource type from %s correctly to %s', (path, type) => {
|
||||
const route = createTestRouteLocation({ path });
|
||||
const route = mock<RouteLocationNormalized>({ path });
|
||||
const resourceType = inferResourceTypeFromRoute(route);
|
||||
expect(resourceType).toBe(type);
|
||||
});
|
||||
|
||||
it('should return undefined for resource type if not found', () => {
|
||||
const route = createTestRouteLocation({ path: '/dashboard/settings' });
|
||||
const route = mock<RouteLocationNormalized>({ path: '/dashboard/settings' });
|
||||
const resourceType = inferResourceTypeFromRoute(route);
|
||||
expect(resourceType).toBeUndefined();
|
||||
});
|
||||
|
@ -45,19 +46,21 @@ describe('rbacUtils', () => {
|
|||
|
||||
describe('inferResourceIdFromRoute()', () => {
|
||||
it('should infer resource ID from params.id', () => {
|
||||
const route = createTestRouteLocation({ params: { id: 'abc123' } });
|
||||
const route = mock<RouteLocationNormalized>({ params: { id: 'abc123' } });
|
||||
const resourceId = inferResourceIdFromRoute(route);
|
||||
expect(resourceId).toBe('abc123');
|
||||
});
|
||||
|
||||
it('should infer resource ID from params.name if id is not present', () => {
|
||||
const route = createTestRouteLocation({ params: { name: 'my-resource' } });
|
||||
const route = mock<RouteLocationNormalized>();
|
||||
route.params = { name: 'my-resource' };
|
||||
const resourceId = inferResourceIdFromRoute(route);
|
||||
expect(resourceId).toBe('my-resource');
|
||||
});
|
||||
|
||||
it('should return undefined for resource ID if neither id nor name is present', () => {
|
||||
const route = createTestRouteLocation({ params: {} });
|
||||
const route = mock<RouteLocationNormalized>();
|
||||
route.params = {};
|
||||
const resourceId = inferResourceIdFromRoute(route);
|
||||
expect(resourceId).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import type { Router } from 'vue-router';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { VIEWS } from '@/constants';
|
||||
import type { ITemplatesWorkflowFull } from '@/Interface';
|
||||
import { Telemetry } from '@/plugins/telemetry';
|
||||
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
@ -7,23 +14,33 @@ import { usePostHog } from '@/stores/posthog.store';
|
|||
import type { TemplatesStore } from '@/stores/templates.store';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
|
||||
import {
|
||||
nodeTypeRespondToWebhookV1,
|
||||
nodeTypeShopifyTriggerV1,
|
||||
nodeTypeTelegramV1,
|
||||
nodeTypeTwitterV1,
|
||||
nodeTypeWebhookV1,
|
||||
nodeTypeWebhookV1_1,
|
||||
nodeTypesSet,
|
||||
} from '@/utils/testData/nodeTypeTestData';
|
||||
import {
|
||||
fullCreateApiEndpointTemplate,
|
||||
fullShopifyTelegramTwitterTemplate,
|
||||
} from '@/utils/testData/templateTestData';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { vi } from 'vitest';
|
||||
import type { Router } from 'vue-router';
|
||||
import { nodeTypeTelegram } from '@/utils/testData/nodeTypeTestData';
|
||||
|
||||
const testTemplate1 = mock<ITemplatesWorkflowFull>({
|
||||
id: 1,
|
||||
workflow: {
|
||||
nodes: [],
|
||||
},
|
||||
full: true,
|
||||
});
|
||||
|
||||
export const testTemplate2 = mock<ITemplatesWorkflowFull>({
|
||||
id: 2,
|
||||
workflow: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'Telegram',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
credentials: {
|
||||
telegramApi: 'telegram_habot',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
full: true,
|
||||
});
|
||||
|
||||
describe('templateActions', () => {
|
||||
describe('useTemplateWorkflow', () => {
|
||||
|
@ -80,16 +97,12 @@ describe('templateActions', () => {
|
|||
});
|
||||
|
||||
describe('When feature flag is enabled and template has nodes requiring credentials', () => {
|
||||
const templateId = fullShopifyTelegramTwitterTemplate.id.toString();
|
||||
const templateId = testTemplate2.id.toString();
|
||||
|
||||
beforeEach(async () => {
|
||||
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
|
||||
templatesStore.addWorkflows([fullShopifyTelegramTwitterTemplate]);
|
||||
nodeTypesStore.setNodeTypes([
|
||||
nodeTypeTelegramV1,
|
||||
nodeTypeTwitterV1,
|
||||
nodeTypeShopifyTriggerV1,
|
||||
]);
|
||||
templatesStore.addWorkflows([testTemplate2]);
|
||||
nodeTypesStore.setNodeTypes([nodeTypeTelegram]);
|
||||
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
||||
|
||||
await useTemplateWorkflow({
|
||||
|
@ -113,17 +126,11 @@ describe('templateActions', () => {
|
|||
});
|
||||
|
||||
describe("When feature flag is enabled and template doesn't have nodes requiring credentials", () => {
|
||||
const templateId = fullCreateApiEndpointTemplate.id.toString();
|
||||
const templateId = testTemplate1.id.toString();
|
||||
|
||||
beforeEach(async () => {
|
||||
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
|
||||
templatesStore.addWorkflows([fullCreateApiEndpointTemplate]);
|
||||
nodeTypesStore.setNodeTypes([
|
||||
nodeTypeWebhookV1,
|
||||
nodeTypeWebhookV1_1,
|
||||
nodeTypeRespondToWebhookV1,
|
||||
...Object.values(nodeTypesSet),
|
||||
]);
|
||||
templatesStore.addWorkflows([testTemplate1]);
|
||||
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
|
||||
|
||||
await useTemplateWorkflow({
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { mock } from 'vitest-mock-extended';
|
||||
import type { IWorkflowTemplateNode } from '@/Interface';
|
||||
import {
|
||||
keyFromCredentialTypeAndName,
|
||||
replaceAllTemplateNodeCredentials,
|
||||
} from '@/utils/templates/templateTransforms';
|
||||
import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
||||
|
||||
describe('templateTransforms', () => {
|
||||
describe('replaceAllTemplateNodeCredentials', () => {
|
||||
|
@ -10,7 +11,7 @@ describe('templateTransforms', () => {
|
|||
const nodeTypeProvider = {
|
||||
getNodeType: vitest.fn(),
|
||||
};
|
||||
const node = newWorkflowTemplateNode({
|
||||
const node = mock<IWorkflowTemplateNode>({
|
||||
id: 'twitter',
|
||||
type: 'n8n-nodes-base.twitter',
|
||||
credentials: {
|
||||
|
@ -40,7 +41,7 @@ describe('templateTransforms', () => {
|
|||
const nodeTypeProvider = {
|
||||
getNodeType: vitest.fn(),
|
||||
};
|
||||
const node = newWorkflowTemplateNode({
|
||||
const node = mock<IWorkflowTemplateNode>({
|
||||
id: 'twitter',
|
||||
type: 'n8n-nodes-base.twitter',
|
||||
});
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/**
|
||||
* Credential type test data
|
||||
*/
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
|
||||
export const newCredentialType = (name: string): ICredentialType => ({
|
||||
name,
|
||||
displayName: name,
|
||||
documentationUrl: name,
|
||||
properties: [],
|
||||
});
|
||||
|
||||
export const credentialTypeTelegram = {
|
||||
name: 'telegramApi',
|
||||
displayName: 'Telegram API',
|
||||
documentationUrl: 'telegram',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Chat with the <a href="https://telegram.me/botfather">bot father</a> to obtain the access token',
|
||||
},
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'baseUrl',
|
||||
type: 'string',
|
||||
default: 'https://api.telegram.org',
|
||||
description: 'Base URL for Telegram Bot API',
|
||||
},
|
||||
],
|
||||
test: {
|
||||
request: {
|
||||
baseURL: '={{$credentials.baseUrl}}/bot{{$credentials.accessToken}}',
|
||||
url: '/getMe',
|
||||
},
|
||||
},
|
||||
} satisfies ICredentialType;
|
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,71 @@
|
|||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
ICredentialsResponse,
|
||||
ITemplatesWorkflowFull,
|
||||
IWorkflowTemplateNode,
|
||||
} from '@/Interface';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { keyFromCredentialTypeAndName } from '@/utils/templates/templateTransforms';
|
||||
import { useSetupTemplateStore } from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import * as testData from './setupTemplate.store.testData';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
import {
|
||||
nodeTypeHttpRequest,
|
||||
nodeTypeNextCloud,
|
||||
nodeTypeReadImap,
|
||||
nodeTypeTelegram,
|
||||
nodeTypeTwitter,
|
||||
} from '@/utils/testData/nodeTypeTestData';
|
||||
import * as testData from './setupTemplate.store.testData';
|
||||
|
||||
const mockCredentialsResponse = (id: string) =>
|
||||
mock<ICredentialsResponse>({
|
||||
id,
|
||||
name: 'Telegram account',
|
||||
type: 'telegramApi',
|
||||
});
|
||||
|
||||
const mockCredentialType = (name: string) => mock<ICredentialType>({ name });
|
||||
|
||||
const mockTemplateNode = (name: string, type: string) =>
|
||||
mock<IWorkflowTemplateNode>({
|
||||
name,
|
||||
type,
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
});
|
||||
|
||||
const testTemplate1 = mock<ITemplatesWorkflowFull>({
|
||||
id: 1,
|
||||
workflow: {
|
||||
nodes: [
|
||||
mockTemplateNode('IMAP Email', 'n8n-nodes-base.emailReadImap'),
|
||||
mockTemplateNode('Nextcloud', 'n8n-nodes-base.nextCloud'),
|
||||
],
|
||||
},
|
||||
full: true,
|
||||
});
|
||||
|
||||
const testTemplate2 = mock<ITemplatesWorkflowFull>({
|
||||
id: 2,
|
||||
workflow: {
|
||||
nodes: [
|
||||
{
|
||||
...mockTemplateNode('Telegram', 'n8n-nodes-base.telegram'),
|
||||
credentials: {
|
||||
telegramApi: 'telegram_habot',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
full: true,
|
||||
});
|
||||
|
||||
describe('SetupWorkflowFromTemplateView store', () => {
|
||||
describe('setInitialCredentialsSelection', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -17,18 +76,13 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
);
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||
credentialsStore.setCredentialTypes([mockCredentialType('telegramApi')]);
|
||||
const templatesStore = useTemplatesStore();
|
||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
||||
templatesStore.addWorkflows([testTemplate2]);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
nodeTypesStore.setNodeTypes([
|
||||
testData.nodeTypeTelegramV1,
|
||||
testData.nodeTypeTwitterV1,
|
||||
testData.nodeTypeShopifyTriggerV1,
|
||||
testData.nodeTypeHttpRequestV1,
|
||||
]);
|
||||
nodeTypesStore.setNodeTypes([nodeTypeTelegram, nodeTypeTwitter, nodeTypeHttpRequest]);
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
||||
setupTemplateStore.setTemplateId(testTemplate2.id.toString());
|
||||
});
|
||||
|
||||
it("should select no credentials when there isn't any available", () => {
|
||||
|
@ -43,8 +97,9 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
|
||||
it("should select credential when there's only one", () => {
|
||||
// Setup
|
||||
const credentialId = 'YaSKdvEcT1TSFrrr1';
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
||||
credentialsStore.setCredentials([mockCredentialsResponse(credentialId)]);
|
||||
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
|
||||
|
@ -52,17 +107,14 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
setupTemplateStore.setInitialCredentialSelection();
|
||||
|
||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({
|
||||
[keyFromCredentialTypeAndName('telegramApi', 'telegram_habot')]: 'YaSKdvEcT1TSFrrr1',
|
||||
[keyFromCredentialTypeAndName('telegramApi', 'telegram_habot')]: credentialId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should select no credentials when there are more than 1 available', () => {
|
||||
// Setup
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentials([
|
||||
testData.credentialsTelegram1,
|
||||
testData.credentialsTelegram2,
|
||||
]);
|
||||
credentialsStore.setCredentials([mockCredentialsResponse('1'), mockCredentialsResponse('2')]);
|
||||
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
|
||||
|
@ -83,7 +135,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
])('should not auto-select credentials for %s', (credentialType, auth) => {
|
||||
// Setup
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentialTypes([testData.newCredentialType(credentialType)]);
|
||||
credentialsStore.setCredentialTypes([mockCredentialType(credentialType)]);
|
||||
credentialsStore.setCredentials([
|
||||
testData.newCredential({
|
||||
name: `${credentialType}Credential`,
|
||||
|
@ -128,24 +180,17 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
|||
|
||||
// Setup
|
||||
const credentialsStore = useCredentialsStore();
|
||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||
credentialsStore.setCredentialTypes([mockCredentialType('telegramApi')]);
|
||||
const templatesStore = useTemplatesStore();
|
||||
templatesStore.addWorkflows([testData.fullSaveEmailAttachmentsToNextCloudTemplate]);
|
||||
templatesStore.addWorkflows([testTemplate1]);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
nodeTypesStore.setNodeTypes([
|
||||
testData.nodeTypeReadImapV1,
|
||||
testData.nodeTypeReadImapV2,
|
||||
testData.nodeTypeNextCloudV1,
|
||||
]);
|
||||
nodeTypesStore.setNodeTypes([nodeTypeReadImap, nodeTypeNextCloud]);
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
setupTemplateStore.setTemplateId(
|
||||
testData.fullSaveEmailAttachmentsToNextCloudTemplate.id.toString(),
|
||||
);
|
||||
setupTemplateStore.setTemplateId(testTemplate1.id.toString());
|
||||
});
|
||||
|
||||
const templateImapNode = testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[0];
|
||||
const templateNextcloudNode =
|
||||
testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[1];
|
||||
const templateImapNode = testTemplate1.workflow.nodes[0];
|
||||
const templateNextcloudNode = testTemplate1.workflow.nodes[1];
|
||||
|
||||
it('should return correct credential usages', () => {
|
||||
const setupTemplateStore = useSetupTemplateStore();
|
||||
|
|
|
@ -53,52 +53,3 @@ export const newCredential = (
|
|||
name: faker.commerce.productName(),
|
||||
...opts,
|
||||
});
|
||||
|
||||
export const credentialsTelegram1: ICredentialsResponse = {
|
||||
createdAt: '2023-11-23T14:26:07.969Z',
|
||||
updatedAt: '2023-11-23T14:26:07.964Z',
|
||||
id: 'YaSKdvEcT1TSFrrr1',
|
||||
name: 'Telegram account',
|
||||
type: 'telegramApi',
|
||||
ownedBy: {
|
||||
id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f',
|
||||
email: 'user@n8n.io',
|
||||
firstName: 'Player',
|
||||
lastName: 'One',
|
||||
},
|
||||
sharedWithProjects: [],
|
||||
};
|
||||
|
||||
export const credentialsTelegram2: ICredentialsResponse = {
|
||||
createdAt: '2023-11-23T14:26:07.969Z',
|
||||
updatedAt: '2023-11-23T14:26:07.964Z',
|
||||
id: 'YaSKdvEcT1TSFrrr2',
|
||||
name: 'Telegram account',
|
||||
type: 'telegramApi',
|
||||
ownedBy: {
|
||||
id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f',
|
||||
email: 'user@n8n.io',
|
||||
firstName: 'Player',
|
||||
lastName: 'One',
|
||||
},
|
||||
sharedWithProjects: [],
|
||||
};
|
||||
|
||||
export {
|
||||
fullSaveEmailAttachmentsToNextCloudTemplate,
|
||||
fullShopifyTelegramTwitterTemplate,
|
||||
} from '@/utils/testData/templateTestData';
|
||||
|
||||
export { credentialTypeTelegram, newCredentialType } from '@/utils/testData/credentialTypeTestData';
|
||||
|
||||
export {
|
||||
nodeTypeHttpRequestV1,
|
||||
nodeTypeNextCloudV1,
|
||||
nodeTypeReadImapV1,
|
||||
nodeTypeReadImapV2,
|
||||
nodeTypeShopifyTriggerV1,
|
||||
nodeTypeTelegramV1,
|
||||
nodeTypeTelegramV1_1,
|
||||
nodeTypeTwitterV1,
|
||||
nodeTypeTwitterV2,
|
||||
} from '@/utils/testData/nodeTypeTestData';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { mock } from 'vitest-mock-extended';
|
||||
import type { IWorkflowTemplateNode } from '@/Interface';
|
||||
import { keyFromCredentialTypeAndName } from '@/utils/templates/templateTransforms';
|
||||
import type { IWorkflowTemplateNodeWithCredentials } from '@/utils/templates/templateTransforms';
|
||||
import type { CredentialUsages } from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState';
|
||||
|
@ -5,8 +7,7 @@ import {
|
|||
getAppCredentials,
|
||||
groupNodeCredentialsByKey,
|
||||
} from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState';
|
||||
import * as testData from './setupTemplate.store.testData';
|
||||
import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
||||
import { nodeTypeTelegram, nodeTypeTwitter } from '@/utils/testData/nodeTypeTestData';
|
||||
|
||||
const objToMap = <TKey extends string, T>(obj: Record<TKey, T>) => {
|
||||
return new Map(Object.entries(obj)) as Map<TKey, T>;
|
||||
|
@ -40,7 +41,7 @@ describe('useCredentialSetupState', () => {
|
|||
groupNodeCredentialsByKey([
|
||||
{
|
||||
node: nodesByName.Twitter,
|
||||
requiredCredentials: testData.nodeTypeTwitterV1.credentials,
|
||||
requiredCredentials: nodeTypeTwitter.credentials!,
|
||||
},
|
||||
]),
|
||||
).toEqual(
|
||||
|
@ -58,14 +59,14 @@ describe('useCredentialSetupState', () => {
|
|||
|
||||
it('returns credentials grouped when the credential names are the same', () => {
|
||||
const [node1, node2] = [
|
||||
newWorkflowTemplateNode({
|
||||
mock<IWorkflowTemplateNode>({
|
||||
id: 'twitter',
|
||||
type: 'n8n-nodes-base.twitter',
|
||||
credentials: {
|
||||
twitterOAuth1Api: 'credential',
|
||||
},
|
||||
}) as IWorkflowTemplateNodeWithCredentials,
|
||||
newWorkflowTemplateNode({
|
||||
mock<IWorkflowTemplateNode>({
|
||||
id: 'telegram',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
credentials: {
|
||||
|
@ -78,11 +79,11 @@ describe('useCredentialSetupState', () => {
|
|||
groupNodeCredentialsByKey([
|
||||
{
|
||||
node: node1,
|
||||
requiredCredentials: testData.nodeTypeTwitterV1.credentials,
|
||||
requiredCredentials: nodeTypeTwitter.credentials!,
|
||||
},
|
||||
{
|
||||
node: node2,
|
||||
requiredCredentials: testData.nodeTypeTelegramV1.credentials,
|
||||
requiredCredentials: nodeTypeTelegram.credentials!,
|
||||
},
|
||||
]),
|
||||
).toEqual(
|
||||
|
|
Loading…
Reference in a new issue