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:
Matsu 2026-06-03 09:08:31 +03:00 committed by GitHub
parent 7ab8254329
commit 0a3d04faa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 133 additions and 162 deletions

View File

@ -1,5 +0,0 @@
/** @type {import('jest').Config} */
module.exports = {
...require('../../../jest.config'),
globalSetup: '<rootDir>/scripts/jest-global-setup.ts',
};

View File

@ -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",

View File

@ -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

View File

@ -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();
};
}

View File

@ -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 = `

View File

@ -1,4 +1,3 @@
import { describe, it, expect, beforeAll } from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { buildCompositeTree } from './composite-builder';
import type {
LeafNode,

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { buildConfigString, type ConfigEntry } from './config-builder';
describe('buildConfigString', () => {

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
AI_CONNECTION_TO_CONFIG_KEY,
AI_CONNECTION_TO_BUILDER,

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import type { CompositeNode } from './composite-tree';
import {
createDeferredConnection,

View File

@ -1,4 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import type { Schema } from 'n8n-workflow';
import { generateSchemaJSDoc, schemaToOutputSample } from './execution-schema-jsdoc';

View File

@ -1,4 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import type { IRunExecutionData } from 'n8n-workflow';
import { buildNodeExecutionStatus, formatExecutionStatusJSDoc } from './execution-status';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { buildExpressionAnnotations } from './expression-annotator';
import type { ExpressionValue } from './types';

View File

@ -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';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { generateWorkflowCode } from './index';
import type { WorkflowJSON } from '../types/base';

View File

@ -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';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
isTriggerType,
isStickyNote,

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { buildSemanticGraph } from './semantic-graph';
import type { WorkflowJSON } from '../types/base';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
getOutputName,
getInputName,

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { escapeString, needsQuoting, formatKey, escapeRegexChars } from './string-utils';
describe('string-utils', () => {

View File

@ -1,4 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import type { IDataObject } from 'n8n-workflow';
import { generateSubnodeCall, generateSubnodesConfig, formatValue } from './subnode-generator';

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import { RESERVED_KEYWORDS, toVarName, getVarName, getUniqueVarName } from './variable-names';
describe('variable-names', () => {

View File

@ -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]));

View File

@ -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', () => {

View File

@ -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');

View File

@ -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['"]\)/);
});
});
});

View File

@ -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');

View File

@ -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',

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
GRID_SIZE,
DEFAULT_NODE_SIZE,

View File

@ -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([

View File

@ -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,

View File

@ -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, {

View File

@ -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,

View File

@ -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);
});
});

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
JS_METHODS,
filterMethodsFromPath,

View File

@ -1,5 +1,3 @@
import { describe, it, expect } from '@jest/globals';
import {
containsExpression,
containsMalformedExpression,

View File

@ -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"],

View 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'],
});

View File

@ -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: