mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 10:39:23 +02:00
test: Migrate @n8n/workflow-sdk from Jest to Vitest (no-changelog) (#31546)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ab8254329
commit
0a3d04faa2
|
|
@ -1,5 +0,0 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...require('../../../jest.config'),
|
||||
globalSetup: '<rootDir>/scripts/jest-global-setup.ts',
|
||||
};
|
||||
|
|
@ -50,9 +50,9 @@
|
|||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"extract-workflows": "npx tsx scripts/extract-workflows.ts",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"generate-types": "npx tsx src/generate-types/generate-types.ts",
|
||||
"fetch-workflows": "npx tsx scripts/fetch-test-workflows.ts",
|
||||
"create-workflows-zip": "npx tsx scripts/create-workflows-zip.ts",
|
||||
|
|
@ -87,9 +87,12 @@
|
|||
"devDependencies": {
|
||||
"@n8n/eslint-plugin-community-nodes": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
"adm-zip": "^0.5.16"
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"adm-zip": "^0.5.16",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* test-fixtures/real-workflows/public_published_templates.zip → test-fixtures/real-workflows/
|
||||
*
|
||||
* Runs automatically via Jest's globalSetup (see scripts/jest-global-setup.ts).
|
||||
* Runs automatically via Vitest's globalSetup (see scripts/vitest-global-setup.ts).
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/extract-workflows.ts
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Jest globalSetup hook: extract committed zips into the expected on-disk
|
||||
* Vitest globalSetup hook: extract committed zips into the expected on-disk
|
||||
* layout before any test runs. We can't rely on npm/pnpm's pretest hook here
|
||||
* because CI invokes `test:unit` (turbo task), not `test`.
|
||||
*/
|
||||
import { extractAllWorkflows } from './extract-workflows';
|
||||
|
||||
module.exports = () => {
|
||||
export default function setup() {
|
||||
extractAllWorkflows();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* Unit tests for the AST interpreter.
|
||||
*/
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import {
|
||||
InterpreterError,
|
||||
SecurityError,
|
||||
|
|
@ -13,71 +15,71 @@ import { interpretSDKCode } from './interpreter';
|
|||
import { parseSDKCode } from './parser';
|
||||
|
||||
/** Helper to get the first call argument from a Jest mock with proper typing */
|
||||
function getFirstCallArg<T>(mockFn: jest.Mock): T {
|
||||
function getFirstCallArg<T>(mockFn: Mock): T {
|
||||
const calls = mockFn.mock.calls as unknown[][];
|
||||
return calls[0][0] as T;
|
||||
}
|
||||
|
||||
// Mock SDK functions for testing
|
||||
const createMockSDKFunctions = (): SDKFunctions => ({
|
||||
workflow: jest.fn((id: string, name: string) => ({
|
||||
workflow: vi.fn((id: string, name: string) => ({
|
||||
id,
|
||||
name,
|
||||
nodes: [] as unknown[],
|
||||
add: jest.fn(function (this: { nodes: unknown[] }, node: unknown) {
|
||||
add: vi.fn(function (this: { nodes: unknown[] }, node: unknown) {
|
||||
this.nodes.push(node);
|
||||
return this;
|
||||
}),
|
||||
then: jest.fn(function (this: { nodes: unknown[] }, node: unknown) {
|
||||
then: vi.fn(function (this: { nodes: unknown[] }, node: unknown) {
|
||||
this.nodes.push(node);
|
||||
return this;
|
||||
}),
|
||||
toJSON: jest.fn(function (this: { id: string; name: string; nodes: unknown[] }) {
|
||||
toJSON: vi.fn(function (this: { id: string; name: string; nodes: unknown[] }) {
|
||||
return { id: this.id, name: this.name, nodes: this.nodes };
|
||||
}),
|
||||
})),
|
||||
node: jest.fn((config: unknown) => ({
|
||||
node: vi.fn((config: unknown) => ({
|
||||
type: 'node',
|
||||
config,
|
||||
then: jest.fn((target: unknown) => target),
|
||||
to: jest.fn((target: unknown) => target),
|
||||
input: jest.fn(() => ({ index: 0 })),
|
||||
output: jest.fn(() => ({ index: 0 })),
|
||||
onError: jest.fn(),
|
||||
then: vi.fn((target: unknown) => target),
|
||||
to: vi.fn((target: unknown) => target),
|
||||
input: vi.fn(() => ({ index: 0 })),
|
||||
output: vi.fn(() => ({ index: 0 })),
|
||||
onError: vi.fn(),
|
||||
})),
|
||||
trigger: jest.fn((config: unknown) => ({
|
||||
trigger: vi.fn((config: unknown) => ({
|
||||
type: 'trigger',
|
||||
config,
|
||||
then: jest.fn((target: unknown) => target),
|
||||
to: jest.fn((target: unknown) => target),
|
||||
then: vi.fn((target: unknown) => target),
|
||||
to: vi.fn((target: unknown) => target),
|
||||
})),
|
||||
sticky: jest.fn((content: string, options?: unknown) => ({
|
||||
sticky: vi.fn((content: string, options?: unknown) => ({
|
||||
type: 'sticky',
|
||||
content,
|
||||
options,
|
||||
})),
|
||||
placeholder: jest.fn((value: string) => `<__PLACEHOLDER_VALUE__${value}__>`),
|
||||
newCredential: jest.fn((name: string) => ({ __newCredential: true, name })),
|
||||
ifElse: jest.fn(),
|
||||
switchCase: jest.fn(),
|
||||
merge: jest.fn((config: unknown) => ({ type: 'merge', config, input: jest.fn() })),
|
||||
splitInBatches: jest.fn(),
|
||||
nextBatch: jest.fn(),
|
||||
languageModel: jest.fn((config: unknown) => ({ type: 'languageModel', config })),
|
||||
memory: jest.fn((config: unknown) => ({ type: 'memory', config })),
|
||||
tool: jest.fn((config: unknown) => ({ type: 'tool', config })),
|
||||
outputParser: jest.fn((config: unknown) => ({ type: 'outputParser', config })),
|
||||
embedding: jest.fn((config: unknown) => ({ type: 'embedding', config })),
|
||||
embeddings: jest.fn((config: unknown) => ({ type: 'embeddings', config })),
|
||||
vectorStore: jest.fn((config: unknown) => ({ type: 'vectorStore', config })),
|
||||
retriever: jest.fn((config: unknown) => ({ type: 'retriever', config })),
|
||||
documentLoader: jest.fn((config: unknown) => ({ type: 'documentLoader', config })),
|
||||
textSplitter: jest.fn((config: unknown) => ({ type: 'textSplitter', config })),
|
||||
reranker: jest.fn((config: unknown) => ({ type: 'reranker', config })),
|
||||
fromAi: jest.fn(
|
||||
placeholder: vi.fn((value: string) => `<__PLACEHOLDER_VALUE__${value}__>`),
|
||||
newCredential: vi.fn((name: string) => ({ __newCredential: true, name })),
|
||||
ifElse: vi.fn(),
|
||||
switchCase: vi.fn(),
|
||||
merge: vi.fn((config: unknown) => ({ type: 'merge', config, input: vi.fn() })),
|
||||
splitInBatches: vi.fn(),
|
||||
nextBatch: vi.fn(),
|
||||
languageModel: vi.fn((config: unknown) => ({ type: 'languageModel', config })),
|
||||
memory: vi.fn((config: unknown) => ({ type: 'memory', config })),
|
||||
tool: vi.fn((config: unknown) => ({ type: 'tool', config })),
|
||||
outputParser: vi.fn((config: unknown) => ({ type: 'outputParser', config })),
|
||||
embedding: vi.fn((config: unknown) => ({ type: 'embedding', config })),
|
||||
embeddings: vi.fn((config: unknown) => ({ type: 'embeddings', config })),
|
||||
vectorStore: vi.fn((config: unknown) => ({ type: 'vectorStore', config })),
|
||||
retriever: vi.fn((config: unknown) => ({ type: 'retriever', config })),
|
||||
documentLoader: vi.fn((config: unknown) => ({ type: 'documentLoader', config })),
|
||||
textSplitter: vi.fn((config: unknown) => ({ type: 'textSplitter', config })),
|
||||
reranker: vi.fn((config: unknown) => ({ type: 'reranker', config })),
|
||||
fromAi: vi.fn(
|
||||
(key: string, desc?: string) => `={{ $fromAI('${key}'${desc ? `, '${desc}'` : ''}) }}`,
|
||||
),
|
||||
nodeJson: jest.fn((node: { name: string } | string, path: string) => {
|
||||
nodeJson: vi.fn((node: { name: string } | string, path: string) => {
|
||||
const name = typeof node === 'string' ? node : node.name;
|
||||
return `={{ $('${name}').item.json.${path} }}`;
|
||||
}),
|
||||
|
|
@ -101,7 +103,7 @@ describe('AST Interpreter', () => {
|
|||
const code = 'const x = {;';
|
||||
try {
|
||||
parseSDKCode(code);
|
||||
fail('Should have thrown');
|
||||
expect.fail('Should have thrown');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(InterpreterError);
|
||||
expect((error as InterpreterError).location).toBeDefined();
|
||||
|
|
@ -714,7 +716,7 @@ describe('AST Interpreter', () => {
|
|||
// Verify node was called with the subnode
|
||||
expect(sdkFunctions.node).toHaveBeenCalled();
|
||||
const nodeCallArgs = getFirstCallArg<{ config: { subnodes: { model: unknown } } }>(
|
||||
sdkFunctions.node as jest.Mock,
|
||||
sdkFunctions.node as Mock,
|
||||
);
|
||||
expect(nodeCallArgs.config.subnodes.model).toBeDefined();
|
||||
});
|
||||
|
|
@ -760,7 +762,7 @@ describe('AST Interpreter', () => {
|
|||
// Verify tool was called with the fromAi result
|
||||
expect(sdkFunctions.tool).toHaveBeenCalled();
|
||||
const toolCallArgs = getFirstCallArg<{ config: { parameters: { sendTo: string } } }>(
|
||||
sdkFunctions.tool as jest.Mock,
|
||||
sdkFunctions.tool as Mock,
|
||||
);
|
||||
expect(toolCallArgs.config.parameters.sendTo).toContain('$fromAI');
|
||||
});
|
||||
|
|
@ -805,8 +807,8 @@ describe('AST Interpreter', () => {
|
|||
});
|
||||
|
||||
it('should allow connect() method on workflow builder', () => {
|
||||
const connectMock = jest.fn();
|
||||
sdkFunctions.workflow = jest.fn(() => ({
|
||||
const connectMock = vi.fn();
|
||||
sdkFunctions.workflow = vi.fn(() => ({
|
||||
connect: connectMock,
|
||||
}));
|
||||
const code = `
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { describe, it, expect, beforeAll } from '@jest/globals';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildCompositeTree } from './composite-builder';
|
||||
import type {
|
||||
LeafNode,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildErrorHandler, hasErrorOutput, getErrorOutputTargets } from './error-handler';
|
||||
import type { OnError } from '../../types/base';
|
||||
import type { CompositeNode, LeafNode } from '../composite-tree';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildIfElseComposite, type BuildContext } from './if-else-handler';
|
||||
import type { CompositeNode, LeafNode } from '../composite-tree';
|
||||
import type { SemanticGraph, SemanticNode } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildMergeComposite, type BuildContext } from './merge-handler';
|
||||
import type { CompositeNode } from '../composite-tree';
|
||||
import type { SemanticGraph, SemanticNode, SourceInfo } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildSplitInBatchesComposite, type BuildContext } from './sib-handler';
|
||||
import type { CompositeNode, LeafNode } from '../composite-tree';
|
||||
import type { SemanticGraph, SemanticNode, SourceInfo } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildSwitchCaseComposite, type BuildContext } from './switch-case-handler';
|
||||
import type { CompositeNode, LeafNode } from '../composite-tree';
|
||||
import type { SemanticGraph, SemanticNode } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildConfigString, type ConfigEntry } from './config-builder';
|
||||
|
||||
describe('buildConfigString', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
AI_CONNECTION_TO_CONFIG_KEY,
|
||||
AI_CONNECTION_TO_BUILDER,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import type { CompositeNode } from './composite-tree';
|
||||
import {
|
||||
createDeferredConnection,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
import type { Schema } from 'n8n-workflow';
|
||||
|
||||
import { generateSchemaJSDoc, schemaToOutputSample } from './execution-schema-jsdoc';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
import type { IRunExecutionData } from 'n8n-workflow';
|
||||
|
||||
import { buildNodeExecutionStatus, formatExecutionStatusJSDoc } from './execution-status';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildExpressionAnnotations } from './expression-annotator';
|
||||
import type { ExpressionValue } from './types';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { annotateGraph } from './graph-annotator';
|
||||
import { buildSemanticGraph } from './semantic-graph';
|
||||
import type { WorkflowJSON } from '../types/base';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { generateWorkflowCode } from './index';
|
||||
import type { WorkflowJSON } from '../types/base';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* Roundtrip tests using the NEW codegen implementation.
|
||||
* This tests if the new codegen can replace the old one.
|
||||
*/
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { generateWorkflowCode } from './index';
|
||||
import { parseWorkflowCode } from './parse-workflow-code';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
isTriggerType,
|
||||
isStickyNote,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { buildSemanticGraph } from './semantic-graph';
|
||||
import type { WorkflowJSON } from '../types/base';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
getOutputName,
|
||||
getInputName,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { escapeString, needsQuoting, formatKey, escapeRegexChars } from './string-utils';
|
||||
|
||||
describe('string-utils', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import { generateSubnodeCall, generateSubnodesConfig, formatValue } from './subnode-generator';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import { RESERVED_KEYWORDS, toVarName, getVarName, getUniqueVarName } from './variable-names';
|
||||
|
||||
describe('variable-names', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Tests for generate-node-defs-cli
|
||||
*
|
||||
* Run with: cd packages/@n8n/workflow-sdk && pnpm jest generate-node-defs-cli
|
||||
* Run with: cd packages/@n8n/workflow-sdk && pnpm test generate-node-defs-cli
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
|
@ -108,7 +108,7 @@ describe('generate-node-defs-cli', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Second run: should skip (files unchanged)
|
||||
const consoleSpy = jest.spyOn(console, 'log');
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await generateNodeDefinitions({ nodesJsonPath, outputDir });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('up to date'));
|
||||
|
|
@ -162,7 +162,7 @@ describe('generate-node-defs-cli', () => {
|
|||
await fs.promises.unlink(hashFile);
|
||||
|
||||
// Should regenerate (not skip)
|
||||
const consoleSpy = jest.spyOn(console, 'log');
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
await generateNodeDefinitions({ nodesJsonPath, outputDir });
|
||||
|
||||
const logCalls = consoleSpy.mock.calls.map((c) => String(c[0]));
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import {
|
|||
} from './pin-data-utils';
|
||||
|
||||
// Mock the generate-types module used by discoverOutputSchemaForNode
|
||||
const mockDiscoverSchemasForNode = jest.fn();
|
||||
const mockFindSchemaForOperation = jest.fn();
|
||||
const mockGenerateJsonSchemaFromData = jest.fn();
|
||||
const mockDiscoverSchemasForNode = vi.fn();
|
||||
const mockFindSchemaForOperation = vi.fn();
|
||||
const mockGenerateJsonSchemaFromData = vi.fn();
|
||||
|
||||
jest.mock('./generate-types', () => ({
|
||||
vi.mock('./generate-types', () => ({
|
||||
discoverSchemasForNode: (...args: unknown[]) => mockDiscoverSchemasForNode(...args) as unknown,
|
||||
findSchemaForOperation: (...args: unknown[]) => mockFindSchemaForOperation(...args) as unknown,
|
||||
generateJsonSchemaFromData: (...args: unknown[]) =>
|
||||
|
|
@ -84,7 +84,7 @@ describe('needsPinData', () => {
|
|||
|
||||
describe('discoverOutputSchemaForNode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns undefined for empty node type', () => {
|
||||
|
|
@ -148,7 +148,7 @@ describe('discoverOutputSchemaForNode', () => {
|
|||
|
||||
describe('inferSchemasFromRunData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty object for empty run data', () => {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ describe('Type Aliases', () => {
|
|||
id: 'test',
|
||||
name: 'Test',
|
||||
config: {},
|
||||
then: jest.fn(),
|
||||
to: jest.fn(),
|
||||
input: jest.fn(),
|
||||
output: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
then: vi.fn(),
|
||||
to: vi.fn(),
|
||||
input: vi.fn(),
|
||||
output: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
getConnections: () => [],
|
||||
update: jest.fn(),
|
||||
update: vi.fn(),
|
||||
} as unknown as AnyNode;
|
||||
|
||||
expect(node.type).toBe('n8n-nodes-base.set');
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ describe('Code Node Helpers', () => {
|
|||
return [{ json: { data: ctx('Config').json } }];
|
||||
});
|
||||
|
||||
// ctx('Config') should become $('Config')
|
||||
expect(result.jsCode).toContain("$('Config')");
|
||||
expect(result.jsCode).not.toContain("ctx('Config')");
|
||||
// ctx('Config') should become $('Config') (quote style depends on the transpiler)
|
||||
expect(result.jsCode).toMatch(/\$\(['"]Config['"]\)/);
|
||||
expect(result.jsCode).not.toMatch(/ctx\(['"]Config['"]\)/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ import * as path from 'path';
|
|||
import { setSchemaBaseDirs, getSchemaBaseDirs } from './schema-validator';
|
||||
import { generateNodeDefinitions } from '../generate-types/generate-node-defs-cli';
|
||||
|
||||
// Use a worker-specific directory to prevent race conditions when multiple Jest
|
||||
// Use a worker-specific directory to prevent race conditions when multiple test
|
||||
// workers run schema-using tests in parallel (they would otherwise concurrently
|
||||
// delete and regenerate schemas into the same shared temp directory).
|
||||
const WORKER_ID = process.env.JEST_WORKER_ID ?? '0';
|
||||
// Vitest exposes the per-worker id as VITEST_POOL_ID; Jest used JEST_WORKER_ID.
|
||||
// Without this, every parallel worker collapses onto the same `-0` directory and
|
||||
// one worker's rmSync wipes schemas another worker just generated.
|
||||
const WORKER_ID = process.env.VITEST_POOL_ID ?? process.env.JEST_WORKER_ID ?? '0';
|
||||
const SCHEMA_TEST_DIR = path.join(os.tmpdir(), `n8n-schema-tests-${WORKER_ID}`);
|
||||
const STAMP_FILE = path.join(SCHEMA_TEST_DIR, '.generator-hash');
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function createMockValidator(
|
|||
id,
|
||||
name: `Mock Validator ${id}`,
|
||||
nodeTypes,
|
||||
validateNode: jest.fn(validateNodeFn),
|
||||
validateNode: vi.fn(validateNodeFn),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
|
||||
describe('validate() with plugins', () => {
|
||||
it('runs registered validators for matching node types', () => {
|
||||
const mockValidateNode = jest.fn().mockReturnValue([]);
|
||||
const mockValidateNode = vi.fn().mockReturnValue([]);
|
||||
const mockValidator = createMockValidator(
|
||||
'test:mock',
|
||||
['n8n-nodes-base.set'],
|
||||
|
|
@ -301,7 +301,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
|
||||
describe('add() with composite handlers', () => {
|
||||
it('delegates IfElseComposite to registered handler when handler handles it', () => {
|
||||
const mockAddNodes = jest.fn().mockReturnValue('If Node');
|
||||
const mockAddNodes = vi.fn().mockReturnValue('If Node');
|
||||
const mockHandler: CompositeHandlerPlugin<IfElseComposite> = {
|
||||
id: 'test:if-else',
|
||||
name: 'Test If/Else Handler',
|
||||
|
|
@ -403,7 +403,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
|
||||
describe('then() with composite handlers', () => {
|
||||
it('delegates IfElseComposite to registered handler in then()', () => {
|
||||
const mockAddNodes = jest
|
||||
const mockAddNodes = vi
|
||||
.fn()
|
||||
.mockImplementation((input: IfElseComposite, ctx: MutablePluginContext) => {
|
||||
ctx.addNodeWithSubnodes(input.ifNode);
|
||||
|
|
@ -442,7 +442,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
// Import the global registry to spy on it
|
||||
|
||||
// Spy on the global registry's findCompositeHandler method
|
||||
const findCompositeHandlerSpy = jest.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
const findCompositeHandlerSpy = vi.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
|
||||
// Create a workflow WITHOUT explicitly passing a registry
|
||||
// Use ifElse composite which should trigger findCompositeHandler
|
||||
|
|
@ -473,7 +473,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
});
|
||||
|
||||
it('uses global pluginRegistry.findCompositeHandler in then() when no registry is provided', () => {
|
||||
const findCompositeHandlerSpy = jest.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
const findCompositeHandlerSpy = vi.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
|
||||
const startTrigger = trigger({
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
|
|
@ -613,7 +613,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
it('ifElse builder is handled by global pluginRegistry handler', () => {
|
||||
registerDefaultPlugins(pluginRegistry);
|
||||
|
||||
const findHandlerSpy = jest.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
const findHandlerSpy = vi.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
|
||||
const trueBranch = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
|
|
@ -646,7 +646,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
it('switchCase builder is handled by global pluginRegistry handler', () => {
|
||||
registerDefaultPlugins(pluginRegistry);
|
||||
|
||||
const findHandlerSpy = jest.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
const findHandlerSpy = vi.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
|
||||
const case0 = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
|
|
@ -679,7 +679,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
it('splitInBatches builder is handled by global pluginRegistry handler', () => {
|
||||
registerDefaultPlugins(pluginRegistry);
|
||||
|
||||
const findHandlerSpy = jest.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
const findHandlerSpy = vi.spyOn(pluginRegistry, 'findCompositeHandler');
|
||||
|
||||
const doneNode = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
|
|
@ -774,7 +774,7 @@ describe('WorkflowBuilder plugin integration', () => {
|
|||
});
|
||||
|
||||
it('handler.getHeadNodeName is called by resolveCompositeHeadName', () => {
|
||||
const mockGetHeadNodeName = jest.fn().mockReturnValue('Custom Head');
|
||||
const mockGetHeadNodeName = vi.fn().mockReturnValue('Custom Head');
|
||||
const customHandler: CompositeHandlerPlugin<{ custom: true }> = {
|
||||
id: 'test:custom',
|
||||
name: 'Custom Handler',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
GRID_SIZE,
|
||||
DEFAULT_NODE_SIZE,
|
||||
|
|
|
|||
|
|
@ -58,20 +58,20 @@ describe('branch-handler-utils', () => {
|
|||
|
||||
describe('collectFromTarget', () => {
|
||||
it('should not call collector for null', () => {
|
||||
const collector = jest.fn();
|
||||
const collector = vi.fn();
|
||||
collectFromTarget(null, collector);
|
||||
expect(collector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call collector for undefined', () => {
|
||||
const collector = jest.fn();
|
||||
const collector = vi.fn();
|
||||
collectFromTarget(undefined, collector);
|
||||
expect(collector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call collector for single node', () => {
|
||||
const node = createMockNode('TestNode');
|
||||
const collector = jest.fn();
|
||||
const collector = vi.fn();
|
||||
collectFromTarget(node, collector);
|
||||
expect(collector).toHaveBeenCalledWith(node);
|
||||
});
|
||||
|
|
@ -79,7 +79,7 @@ describe('branch-handler-utils', () => {
|
|||
it('should call collector for each node in array', () => {
|
||||
const node1 = createMockNode('Node1');
|
||||
const node2 = createMockNode('Node2');
|
||||
const collector = jest.fn();
|
||||
const collector = vi.fn();
|
||||
collectFromTarget([node1, node2], collector);
|
||||
expect(collector).toHaveBeenCalledTimes(2);
|
||||
expect(collector).toHaveBeenCalledWith(node1);
|
||||
|
|
@ -88,7 +88,7 @@ describe('branch-handler-utils', () => {
|
|||
|
||||
it('should skip null elements in array', () => {
|
||||
const node1 = createMockNode('Node1');
|
||||
const collector = jest.fn();
|
||||
const collector = vi.fn();
|
||||
collectFromTarget([node1, null, null], collector);
|
||||
expect(collector).toHaveBeenCalledTimes(1);
|
||||
expect(collector).toHaveBeenCalledWith(node1);
|
||||
|
|
@ -98,7 +98,7 @@ describe('branch-handler-utils', () => {
|
|||
describe('addBranchTargetNodes', () => {
|
||||
it('should do nothing for null', () => {
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn(),
|
||||
addBranchToGraph: vi.fn(),
|
||||
} as unknown as MutablePluginContext;
|
||||
addBranchTargetNodes(null, ctx);
|
||||
expect(ctx.addBranchToGraph).not.toHaveBeenCalled();
|
||||
|
|
@ -107,7 +107,7 @@ describe('branch-handler-utils', () => {
|
|||
it('should call addBranchToGraph for single target', () => {
|
||||
const node = createMockNode('TestNode');
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn(),
|
||||
addBranchToGraph: vi.fn(),
|
||||
} as unknown as MutablePluginContext;
|
||||
addBranchTargetNodes(node, ctx);
|
||||
expect(ctx.addBranchToGraph).toHaveBeenCalledWith(node);
|
||||
|
|
@ -117,7 +117,7 @@ describe('branch-handler-utils', () => {
|
|||
const node1 = createMockNode('Node1');
|
||||
const node2 = createMockNode('Node2');
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn(),
|
||||
addBranchToGraph: vi.fn(),
|
||||
} as unknown as MutablePluginContext;
|
||||
addBranchTargetNodes([node1, node2], ctx);
|
||||
expect(ctx.addBranchToGraph).toHaveBeenCalledTimes(2);
|
||||
|
|
@ -128,7 +128,7 @@ describe('branch-handler-utils', () => {
|
|||
it('should skip null branch', () => {
|
||||
const mainConns = new Map<number, ConnectionTarget[]>();
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn(),
|
||||
addBranchToGraph: vi.fn(),
|
||||
} as unknown as MutablePluginContext;
|
||||
processBranchForComposite(null, 0, ctx, mainConns);
|
||||
expect(mainConns.size).toBe(0);
|
||||
|
|
@ -138,7 +138,7 @@ describe('branch-handler-utils', () => {
|
|||
const node = createMockNode('TestNode');
|
||||
const mainConns = new Map<number, ConnectionTarget[]>();
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn().mockReturnValue('TestNode'),
|
||||
addBranchToGraph: vi.fn().mockReturnValue('TestNode'),
|
||||
} as unknown as MutablePluginContext;
|
||||
processBranchForComposite(node, 0, ctx, mainConns);
|
||||
expect(mainConns.get(0)).toEqual([{ node: 'TestNode', type: 'main', index: 0 }]);
|
||||
|
|
@ -149,7 +149,7 @@ describe('branch-handler-utils', () => {
|
|||
const node2 = createMockNode('Node2');
|
||||
const mainConns = new Map<number, ConnectionTarget[]>();
|
||||
const ctx = {
|
||||
addBranchToGraph: jest.fn().mockImplementation((n: { name: string }) => n.name),
|
||||
addBranchToGraph: vi.fn().mockImplementation((n: { name: string }) => n.name),
|
||||
} as unknown as MutablePluginContext;
|
||||
processBranchForComposite([node1, node2], 0, ctx, mainConns);
|
||||
expect(mainConns.get(0)).toEqual([
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ function createMockContext(): MutablePluginContext {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map(),
|
||||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
nodes.set(branchNode.name, {
|
||||
instance: branchNode,
|
||||
|
|
|
|||
|
|
@ -68,14 +68,14 @@ function createMockContext(): MutablePluginContext {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map(),
|
||||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
nodes.set(branchNode.name, {
|
||||
instance: branchNode,
|
||||
|
|
@ -284,7 +284,7 @@ describe('splitInBatchesHandler', () => {
|
|||
|
||||
// Mock addBranchToGraph to call addNodes recursively (simulating nested SIB)
|
||||
let recursiveCallCount = 0;
|
||||
ctx.addBranchToGraph = jest.fn((branch: unknown) => {
|
||||
ctx.addBranchToGraph = vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
ctx.nodes.set(branchNode.name, {
|
||||
instance: branchNode,
|
||||
|
|
@ -390,7 +390,7 @@ describe('splitInBatchesHandler', () => {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map<string, Map<number, ConnectionTarget[]>>([
|
||||
|
|
@ -399,7 +399,7 @@ describe('splitInBatchesHandler', () => {
|
|||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
if (!nodes.has(branchNode.name)) {
|
||||
nodes.set(branchNode.name, {
|
||||
|
|
@ -460,7 +460,7 @@ describe('splitInBatchesHandler', () => {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map<string, Map<number, ConnectionTarget[]>>([
|
||||
|
|
@ -469,7 +469,7 @@ describe('splitInBatchesHandler', () => {
|
|||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
if (!nodes.has(branchNode.name)) {
|
||||
nodes.set(branchNode.name, {
|
||||
|
|
@ -505,7 +505,7 @@ describe('splitInBatchesHandler', () => {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map<string, Map<number, ConnectionTarget[]>>([
|
||||
|
|
@ -514,7 +514,7 @@ describe('splitInBatchesHandler', () => {
|
|||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
if (!nodes.has(branchNode.name)) {
|
||||
nodes.set(branchNode.name, {
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ function createMockContext(): MutablePluginContext {
|
|||
workflowId: 'test-workflow',
|
||||
workflowName: 'Test Workflow',
|
||||
settings: {},
|
||||
addNodeWithSubnodes: jest.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
addNodeWithSubnodes: vi.fn((node: NodeInstance<string, string, unknown>) => {
|
||||
nodes.set(node.name, {
|
||||
instance: node,
|
||||
connections: new Map(),
|
||||
});
|
||||
return node.name;
|
||||
}),
|
||||
addBranchToGraph: jest.fn((branch: unknown) => {
|
||||
addBranchToGraph: vi.fn((branch: unknown) => {
|
||||
const branchNode = branch as NodeInstance<string, string, unknown>;
|
||||
nodes.set(branchNode.name, {
|
||||
instance: branchNode,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function createMockValidator(id: string, nodeTypes: string[] = [], priority = 0)
|
|||
name: `Mock Validator ${id}`,
|
||||
nodeTypes,
|
||||
priority,
|
||||
validateNode: jest.fn().mockReturnValue([]),
|
||||
validateNode: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ function createMockCompositeHandler(
|
|||
name: `Mock Handler ${id}`,
|
||||
priority,
|
||||
canHandle: canHandleFn as (input: unknown) => input is unknown,
|
||||
addNodes: jest.fn().mockReturnValue('mock-node'),
|
||||
addNodes: vi.fn().mockReturnValue('mock-node'),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ function createMockSerializer(id: string, format: string): SerializerPlugin {
|
|||
id,
|
||||
name: `Mock Serializer ${id}`,
|
||||
format,
|
||||
serialize: jest.fn().mockReturnValue({}),
|
||||
serialize: vi.fn().mockReturnValue({}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -218,12 +218,9 @@ describe('pluginRegistry singleton', () => {
|
|||
expect(pluginRegistry).toBeInstanceOf(PluginRegistry);
|
||||
});
|
||||
|
||||
it('returns the same instance on multiple imports', () => {
|
||||
// Import again to verify singleton - intentionally using require() to test module-level singleton
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Testing CommonJS singleton behavior
|
||||
const { pluginRegistry: anotherRegistry } = require('./registry') as {
|
||||
pluginRegistry: PluginRegistry;
|
||||
};
|
||||
it('returns the same instance on multiple imports', async () => {
|
||||
// Import again to verify the module-level singleton is shared across imports
|
||||
const { pluginRegistry: anotherRegistry } = await import('./registry');
|
||||
expect(anotherRegistry).toBe(pluginRegistry);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
JS_METHODS,
|
||||
filterMethodsFromPath,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
import {
|
||||
containsExpression,
|
||||
containsMalformedExpression,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"types": ["node", "vitest/globals"],
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
|
|
|
|||
7
packages/@n8n/workflow-sdk/vitest.config.ts
Normal file
7
packages/@n8n/workflow-sdk/vitest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { createVitestConfig } from '@n8n/vitest-config/node';
|
||||
|
||||
export default createVitestConfig({
|
||||
// The n8n root jest.config sets `restoreMocks: true`, and the test files rely on it.
|
||||
restoreMocks: true,
|
||||
globalSetup: ['./scripts/vitest-global-setup.ts'],
|
||||
});
|
||||
|
|
@ -2890,15 +2890,24 @@ importers:
|
|||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
'@n8n/vitest-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vitest-config
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.5.7
|
||||
version: 0.5.7
|
||||
'@types/estree':
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user