chore: Remove references to Jest (#33282)

This commit is contained in:
Matsu 2026-07-01 12:03:25 +03:00 committed by GitHub
parent 926a8158c2
commit df97053888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 106 additions and 1859 deletions

View File

@ -24,13 +24,13 @@ Based on the affected area, pick the test layer and pattern:
| Area | Test Layer | Pattern | Key Location |
|------|-----------|---------|--------------|
| Node operation | Jest unit | NodeTestHarness + nock | `packages/nodes-base/nodes/*/test/` |
| Node credential | Jest unit | jest-mock-extended | `packages/nodes-base/nodes/*/test/` |
| Trigger webhook | Jest unit | mock IHookFunctions + jest.mock GenericFunctions | `packages/nodes-base/nodes/*/test/` |
| Binary data | Jest unit | NodeTestHarness assertBinaryData | `packages/core/nodes-testing/` |
| Execution engine | Jest integration | WorkflowRunner + DI container | `packages/cli/src/__tests__/` |
| CLI / API | Jest integration | setupTestServer + supertest | `packages/cli/test/integration/` |
| Config | Jest unit | GlobalConfig + Container | `packages/@n8n/config/src/__tests__/` |
| Node operation | Vitest unit | NodeTestHarness + nock | `packages/nodes-base/nodes/*/test/` |
| Node credential | Vitest unit | vitest-mock-extended | `packages/nodes-base/nodes/*/test/` |
| Trigger webhook | Vitest unit | mock IHookFunctions + vi.mock GenericFunctions | `packages/nodes-base/nodes/*/test/` |
| Binary data | Vitest unit | NodeTestHarness assertBinaryData | `packages/core/nodes-testing/` |
| Execution engine | Vitest integration | WorkflowRunner + DI container | `packages/cli/src/__tests__/` |
| CLI / API | Vitest integration | setupTestServer + supertest | `packages/cli/test/integration/` |
| Config | Vitest unit | GlobalConfig + Container | `packages/@n8n/config/src/__tests__/` |
| Editor UI | Vitest | Vue Test Utils + Pinia | `packages/frontend/editor-ui/src/**/__tests__/` |
| E2E / Canvas | Playwright | Test containers + composables | `packages/testing/playwright/` |

View File

@ -39,7 +39,7 @@ Must be one of the following:
| `test` | Adding missing tests or correcting existing tests | ❌ |
| `docs` | Documentation only changes | ❌ |
| `refactor` | A behavior-neutral code change that neither fixes a bug nor adds a feature | ❌ |
| `build` | Changes that affect the build system or external dependencies (TypeScript, Jest, pnpm, etc.) | ❌ |
| `build` | Changes that affect the build system or external dependencies (TypeScript, Vitest, pnpm, etc.) | ❌ |
| `ci` | Changes to CI configuration files and scripts (e.g. Github actions) | ❌ |
| `chore` | Routine tasks, maintenance, and minor updates not covered by other types | ❌ |

View File

@ -135,10 +135,6 @@ jobs:
packages/cli/test/shared/db/**
packages/cli/test/setup-testcontainers.js
packages/cli/test/teardown-testcontainers.js
packages/cli/jest.config.integration.js
packages/cli/jest.config.integration.testcontainers.js
packages/cli/jest.config.migration.js
packages/cli/jest.config.migration.testcontainers.js
packages/@n8n/db/**
packages/@n8n/backend-test-utils/**
packages/cli/**/__tests__/**

View File

@ -122,7 +122,7 @@ The monorepo is organized into these key packages:
- **Frontend:** Vue 3 + TypeScript + Vite + Pinia + Storybook UI Library
- **Backend:** Node.js + TypeScript + Express + TypeORM
- **Testing:** Jest (unit) + Playwright (E2E)
- **Testing:** Vitest (unit) + Playwright (E2E)
- **Database:** TypeORM with SQLite/PostgreSQL support
- **Code Quality:** Biome (for formatting) + ESLint + lefthook git hooks
@ -201,7 +201,7 @@ const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1);
- **For Vitest packages that use `@n8n/di` decorators**, use `createVitestConfigWithDecorators` from `@n8n/vitest-config/node-decorators`. It enables SWC `decoratorMetadata` (esbuild doesn't emit it) and externalizes workspace packages that register services (`@n8n/di`, `@n8n/config`, `@n8n/constants`, `n8n-workflow`) so a single DI `Container` instance is shared across the runtime. Loading them through Vitest's pipeline alongside their CJS dist produces two `Container`s and `Container.get(...)` returns `undefined`.
What we use for testing and writing tests:
- For testing nodes and other backend components, we use Jest for unit tests. Examples can be found in `packages/nodes-base/nodes/**/*test*`.
- For testing nodes and other backend components, we use Vitest for unit tests. Examples can be found in `packages/nodes-base/nodes/**/*test*`.
- We use `nock` for server mocking
- For frontend we use `vitest`
- For E2E tests we use Playwright. Run with `pnpm --filter=n8n-playwright test:local`.

View File

@ -1,123 +0,0 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('get-tsconfig').getTsconfig().config;
const { resolve } = require('path');
const coverageExcludes = require('./jest.coverage-excludes');
/** @type {import('ts-jest').TsJestTransformerOptions} */
const tsJestOptions = {
tsconfig: {
...compilerOptions,
declaration: false,
sourceMap: true,
rootDir: '.',
// Force the transpile-only path for tests. Without this, ts-jest runs
// the full type-checker on every transformed file, which combined with
// aggressive worker recycling (workerIdleMemoryLimit) makes test runs
// ~4-5x slower. Package builds and `pnpm typecheck` read the package
// tsconfig directly and are unaffected.
isolatedModules: true,
// Relax strictness for cross-package imports. ts-jest applies the host
// package's tsconfig to every file it transforms, including imports
// from packages like nodes-base that disable these in their own
// tsconfig. The host package's own `pnpm typecheck` is unaffected.
noImplicitReturns: false,
noUncheckedIndexedAccess: false,
noImplicitOverride: false,
useUnknownInCatchVariables: false,
},
};
const isCoverageEnabled = process.env.COVERAGE_ENABLED === 'true';
const esmDependencies = [
'pdfjs-dist',
'openid-client',
'oauth4webapi',
'jose',
'p-retry',
'is-network-error',
'uuid',
'change-case',
// file-type@21 and its ESM-only dependency chain
'file-type',
'strtok3',
'token-types',
'uint8array-extras',
'@tokenizer/inflate',
'@borewit/text-codec',
// Add other ESM dependencies that need to be transformed here
];
const esmDependenciesPattern = esmDependencies.join('|');
// On Windows, Jest passes raw backslash paths to transform/ignore pattern regexes
// without normalizing separators, so we must match both / and \.
const isWindows = process.platform === 'win32';
const sep = isWindows ? '[/\\\\]' : '/';
const nonSep = isWindows ? '[^/\\\\]' : '[^/]';
const esmDependenciesRegex = `node_modules${sep}(${esmDependenciesPattern})${sep}.+\\.m?js$`;
/** @type {import('jest').Config} */
const config = {
verbose: false,
testEnvironment: 'node',
testRegex: '\\.(test|spec)\\.(js|ts)$',
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
transform: {
'^.+\\.ts$': ['ts-jest', tsJestOptions],
[esmDependenciesRegex]: [
'babel-jest',
{
presets: ['@babel/preset-env'],
plugins: ['babel-plugin-transform-import-meta'],
},
],
},
transformIgnorePatterns: [
// Exclude the pnpm virtual store dir (.pnpm) and the ESM deps from being ignored.
`${sep}node_modules${sep}(?!\\.pnpm|${esmDependenciesPattern})${sep}`,
// On Windows with pnpm, ESM deps live inside the virtual store (.pnpm) and are
// accessed via hardlinks (no symlinks), so their path goes through
// .pnpm/<pkg@version>/node_modules/<pkg>/... — exclude ESM deps from being ignored there too.
...(isWindows
? [`${sep}\\.pnpm${sep}${nonSep}+${sep}node_modules${sep}(?!${esmDependenciesPattern})${sep}`]
: []),
],
// This resolve the path mappings from the tsconfig relative to each jest.config.js
moduleNameMapper: {
'^@n8n/utils$': resolve(__dirname, 'packages/@n8n/utils/dist/index.cjs'),
// jest-resolve@29 doesn't honor `./lib/*` subpath patterns in @anthropic-ai/sdk's exports map
'^@anthropic-ai/sdk/lib/(.*)$': '@anthropic-ai/sdk/lib/$1.js',
// Core uses vitest-mock-extended (ESM-only) for its own tests; route the
// shim to a CJS-friendly variant for Jest-based consumers (nodes-base, cli).
'^(\\./|\\.\\./nodes-testing/)mock-extended$': resolve(
__dirname,
'packages/core/nodes-testing/mock-extended.jest.cjs',
),
...(compilerOptions?.paths
? pathsToModuleNameMapper(compilerOptions.paths, {
prefix: `<rootDir>${compilerOptions.baseUrl ? `/${compilerOptions.baseUrl.replace(/^\.\//, '')}` : ''}`,
})
: {}),
},
// file-type@21's `exports` map exposes no `require`/string `default` entry,
// only `import`/`module-sync`. Add `module-sync` so jest-resolve can pick its
// entry point (additive to the node env defaults).
testEnvironmentOptions: {
customExportConditions: ['node', 'node-addons', 'module-sync'],
},
setupFilesAfterEnv: ['jest-expect-message'],
restoreMocks: true,
collectCoverage: isCoverageEnabled,
coverageReporters: ['text-summary', 'lcov', 'html-spa'],
workerIdleMemoryLimit: '1MB',
};
if (process.env.CI === 'true') {
config.collectCoverageFrom = ['src/**/*.ts', ...coverageExcludes];
config.reporters = ['default', 'jest-junit'];
config.coverageReporters = ['lcov'];
}
module.exports = config;

View File

@ -1,11 +0,0 @@
// Shared `collectCoverageFrom` exclusions for all jest packages. `**`-anchored
// so they apply regardless of the source roots a package collects from.
// Mirrored by packages/@n8n/vitest-config/coverage-excludes.ts (vitest) and the
// V8 path filter in packages/testing/playwright/coverage-options.ts — keep in sync.
module.exports = [
'!**/*.spec.ts',
'!**/*.test.ts',
'!**/__tests__/**',
'!**/__mocks__/**',
'!**/*.d.ts',
];

View File

@ -45,7 +45,7 @@
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
"start": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n",
"mutate": "node scripts/mutation-health/mutate.mjs",
"test": "JEST_JUNIT_CLASSNAME={filepath} turbo run test",
"test": "turbo run test",
"test:ci": "turbo run test --continue --concurrency=1",
"test:ci:frontend": "turbo run test --continue --filter='./packages/frontend/**'",
"test:ci:frontend:changed": "turbo run test:changed --continue --filter='./packages/frontend/**'",
@ -70,20 +70,12 @@
"@biomejs/biome": "^1.9.0",
"@dotenvx/dotenvx": "^1.40.0",
"@n8n/eslint-config": "workspace:*",
"@types/jest": "^29.5.3",
"@types/node": "*",
"@types/supertest": "^6.0.3",
"babel-jest": "catalog:",
"babel-plugin-transform-import-meta": "^2.3.2",
"cross-env": "^7.0.3",
"eslint": "catalog:",
"get-tsconfig": "catalog:",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-expect-message": "^1.1.3",
"jest-junit": "^16.0.0",
"jest-mock": "^29.6.2",
"jest-mock-extended": "^3.0.4",
"lefthook": "^1.7.15",
"nock": "^14.0.14",
"nodemon": "^3.0.1",
@ -93,7 +85,6 @@
"rimraf": "^5.0.1",
"stylelint": "catalog:",
"supertest": "^7.1.1",
"ts-jest": "^29.4.9",
"tsc-alias": "^1.8.10",
"tsc-watch": "^6.2.0",
"turbo": "2.9.15",

View File

@ -52,8 +52,6 @@ function rewriteSourceRequire(): Plugin {
export default mergeConfig(
createVitestConfig({
// The n8n root jest.config sets `restoreMocks: true`, and test files silently rely on
// it — omit this and mocks bleed between tests.
restoreMocks: true,
// Integration tests run via vitest.integration.config.mjs (real providers, long timeouts).
exclude: [...configDefaults.exclude, '**/__tests__/integration/**'],

View File

@ -4,8 +4,6 @@ import { mergeConfig } from 'vite';
export default mergeConfig(
createVitestConfig({
// The n8n root jest.config sets `restoreMocks: true`, and most test files silently
// rely on it — omit this and mocks bleed between tests.
restoreMocks: true,
}),
{

View File

@ -140,7 +140,7 @@ export async function terminate() {
}
// Clear all cached DI singletons (DbConnection, DataSource, GlobalConfig,
// AuthRolesService, …). With persistent Jest workers (no per-file process
// AuthRolesService, …). With persistent Vitest workers (no per-file process
// recycling), the next test file's testDb.init() would otherwise reuse the
// DbConnection instance whose DataSource we just destroyed — and try to
// .initialize() it again, which hangs. Resetting forces the next get() to

View File

@ -6,7 +6,8 @@
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

View File

@ -1,7 +1,5 @@
import { createVitestConfig } from '@n8n/vitest-config/node';
export default createVitestConfig({
// The n8n root jest.config sets `restoreMocks: true`, and test files silently rely on
// it — omit this and mocks bleed between tests.
restoreMocks: true,
});

View File

@ -2,7 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"rootDir": ".",
"types": ["node", "jest"]
"types": ["node", "vitest/globals"]
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View File

@ -48,6 +48,7 @@
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"vite": "catalog:",
"vitest": "catalog:"
}
}

View File

@ -2,7 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"rootDir": ".",
"types": ["node", "jest"]
"types": ["node", "vitest/globals", "vite/client"]
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View File

@ -1,7 +1,7 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"types": ["node", "jest"],
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},

View File

@ -139,8 +139,6 @@ function tscDecoratorTransform(): Plugin {
export default mergeConfig(
createVitestConfigWithDecorators({
// The n8n root jest.config sets `restoreMocks: true`, and most test files silently
// rely on it — omit this and mocks bleed between tests.
restoreMocks: true,
}),
{

View File

@ -1,7 +1,7 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"types": ["node", "jest"],
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},

View File

@ -17,7 +17,6 @@ export const baseConfig = tseslint.config(
'dist/**',
'eslint.config.mjs',
'tsup.config.ts',
'jest.config.js',
'vite.config.ts',
'vitest.config.ts',
]),

View File

@ -14,7 +14,7 @@ ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule
{
name: 'only safe scripts',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "jest", "dev": "nodemon" } }',
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "vitest", "dev": "nodemon" } }',
},
{
name: 'non-package.json file is ignored',
@ -82,7 +82,7 @@ ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule
{
name: 'mix of allowed and forbidden scripts — only forbidden reported',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "jest" } }',
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "vitest" } }',
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
},
{

View File

@ -383,7 +383,7 @@ The runtime **can only**:
- Test observability emission
- Test error handling
**Integration Tests** (jest in workflow package):
**Integration Tests** (vitest in workflow package):
- Test full stack with real isolated-vm
- Test concurrent evaluations
- Test with real workflow data

View File

@ -39,7 +39,7 @@
"build:unchecked": "pnpm run build",
"create-json-schema": "tsx scripts/create-json-schema.ts",
"preview": "vite preview",
"test": "jest --passWithNoTests"
"test": "node -e \"process.exit(0);\""
},
"peerDependencies": {
"vue": "catalog:frontend",

View File

@ -15,3 +15,14 @@ expect.extend({
};
},
});
interface CustomMatchers<R = unknown> {
toMatchZod(expected: z.ZodTypeAny): R;
}
declare module 'vitest' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- must mirror Vitest's `Matchers<T = any>` default type param
interface Matchers<T = any> extends CustomMatchers<T> {
toMatchZod(expected: z.ZodTypeAny): T;
}
}

View File

@ -1,5 +0,0 @@
namespace jest {
interface Matchers<R, T> {
toMatchZod(expected: unknown): T;
}
}

View File

@ -1,6 +1,5 @@
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,
});

View File

@ -1,8 +1,6 @@
import { createVitestConfig } from '@n8n/vitest-config/node';
export default createVitestConfig({
// The n8n root jest.config sets `restoreMocks: true`, and test files silently rely on
// it — omit this and mocks bleed between tests.
restoreMocks: true,
testTimeout: 30_000,
});

View File

@ -1,6 +1,6 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { mock } from 'vitest-mock-extended';
import { StructuredToolkit } from 'n8n-core';
import {
NodeConnectionTypes,
@ -245,7 +245,7 @@ describe('runtime', () => {
},
],
});
const callTool = jest
const callTool = vi
.spyOn(Client.prototype, 'callTool')
.mockResolvedValue({ content: [{ type: 'text', text: 'ok' }] });
vi.spyOn(Client.prototype, 'close').mockResolvedValue();
@ -280,7 +280,7 @@ describe('runtime', () => {
},
],
});
const callTool = jest
const callTool = vi
.spyOn(Client.prototype, 'callTool')
.mockResolvedValue({ content: [{ type: 'text', text: 'ok' }] });
vi.spyOn(Client.prototype, 'close').mockResolvedValue();

View File

@ -563,7 +563,7 @@ describe('VectorStoreOracleDB.node', () => {
throw new Error('releaseVectorStoreClient was not captured');
}
const clientCloseSpy = jest
const clientCloseSpy = vi
.spyOn(lastInstance.client as { close: () => Promise<void> }, 'close')
.mockResolvedValue(undefined);

View File

@ -364,7 +364,7 @@ describe('MicrosoftAgent365Trigger', () => {
try {
await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);
fail('Should have thrown an error');
expect.fail('Should have thrown an error');
} catch (error) {
expect(error).toBeInstanceOf(NodeOperationError);
expect((error as NodeOperationError).description).toBe(
@ -379,7 +379,7 @@ describe('MicrosoftAgent365Trigger', () => {
try {
await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);
fail('Should have thrown an error');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(NodeOperationError);
expect((err as NodeOperationError).message).toContain('Network error');

View File

@ -6,11 +6,6 @@
* DI mocks unless they need custom behaviour.
*/
// jest-mock-extended (used by NodeTestHarness) requires `jest` as a global.
// Vitest does not set this alias automatically.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).jest = vi;
vi.mock('@n8n/di', () => ({
Container: {
get: vi.fn().mockImplementation(() => ({

View File

@ -40,7 +40,7 @@ describe('embeddingInputValidation', () => {
it('should include helpful error message', () => {
try {
validateEmbedQueryInput(undefined, mockNode);
fail('Expected error to be thrown');
expect.fail('Expected error to be thrown');
} catch (e) {
expect(e).toBeInstanceOf(NodeOperationError);
expect((e as NodeOperationError).message).toContain('empty or undefined text');
@ -50,7 +50,7 @@ describe('embeddingInputValidation', () => {
it('should include description with possible causes', () => {
try {
validateEmbedQueryInput(undefined, mockNode);
fail('Expected error to be thrown');
expect.fail('Expected error to be thrown');
} catch (e) {
expect(e).toBeInstanceOf(NodeOperationError);
const description = (e as NodeOperationError).description as string;
@ -109,7 +109,7 @@ describe('embeddingInputValidation', () => {
it('should include index of invalid document in error message', () => {
try {
validateEmbedDocumentsInput(['valid', 'also valid', undefined], mockNode);
fail('Expected error to be thrown');
expect.fail('Expected error to be thrown');
} catch (e) {
expect(e).toBeInstanceOf(NodeOperationError);
expect((e as NodeOperationError).message).toContain('index 2');
@ -119,7 +119,7 @@ describe('embeddingInputValidation', () => {
it('should include helpful description in error', () => {
try {
validateEmbedDocumentsInput(['valid', null], mockNode);
fail('Expected error to be thrown');
expect.fail('Expected error to be thrown');
} catch (e) {
expect(e).toBeInstanceOf(NodeOperationError);
expect((e as NodeOperationError).description).toContain('non-empty strings');

View File

@ -189,7 +189,7 @@ export class JsTaskRunner extends TaskRunner {
Buffer.allocUnsafe = safeAlloc as typeof Buffer.allocUnsafe;
Buffer.allocUnsafeSlow = safeAlloc as typeof Buffer.allocUnsafeSlow;
// Freeze globals, except in tests because Jest needs to be able to mutate prototypes
// Freeze globals, except in tests because Vitest needs to be able to mutate prototypes
if (process.env.NODE_ENV !== 'test') {
Object.getOwnPropertyNames(globalThis)
.map((name) => Reflect.get(globalThis, name) as unknown)

View File

@ -4,8 +4,6 @@ import { mergeConfig } from 'vite';
export default mergeConfig(
createVitestConfigWithDecorators({
// The n8n root jest.config sets `restoreMocks: true`, and test files silently rely on
// it — omit this and mocks bleed between tests.
restoreMocks: true,
testTimeout: 10_000,
}),

View File

@ -1,6 +1,6 @@
{
"extends": "./tsconfig.common.json",
"compilerOptions": {
"types": ["node", "jest"]
"types": ["node", "vitest/globals"]
}
}

View File

@ -23,6 +23,5 @@
"sourceMap": true,
"skipLibCheck": true,
"tsBuildInfoFile": "${configDir}/dist/typecheck.tsbuildinfo"
},
"files": ["../../../node_modules/jest-expect-message/types/index.d.ts"]
}
}

View File

@ -14,7 +14,7 @@ import type { SDKFunctions } from './interpreter';
import { interpretSDKCode } from './interpreter';
import { parseSDKCode } from './parser';
/** Helper to get the first call argument from a Jest mock with proper typing */
/** Helper to get the first call argument from a Vitest mock with proper typing */
function getFirstCallArg<T>(mockFn: Mock): T {
const calls = mockFn.mock.calls as unknown[][];
return calls[0][0] as T;

View File

@ -117,6 +117,7 @@
"ts-essentials": "^7.0.3",
"tsconfig-paths": "^4.2.0",
"@vitest/coverage-v8": "catalog:",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
},

View File

@ -177,8 +177,6 @@ describe('WorkflowExecuteAdditionalData', () => {
},
nodes: [],
connections: {},
// Real object: a mock-generated fn breaks Workflow's observable wrapping
// (vitest's vi.fn().mock is enumerable; Jest's was not).
staticData: {},
}),
);

View File

@ -371,7 +371,7 @@ describe('enqueueExecution', () => {
const error = new Error('stop for test purposes');
// mock a rejection to stop execution flow before we create the PCancelable promise,
// so that Jest does not move on to tear down the suite until the PCancelable settles
// so that Vitest does not move on to tear down the suite until the PCancelable settles
addJob.mockRejectedValueOnce(error);
// @ts-expect-error Private method
@ -393,7 +393,7 @@ describe('enqueueExecution', () => {
const error = new Error('stop for test purposes');
// mock a rejection to stop execution flow before we create the PCancelable promise,
// so that Jest does not move on to tear down the suite until the PCancelable settles
// so that Vitest does not move on to tear down the suite until the PCancelable settles
addJob.mockRejectedValueOnce(error);
const restartExecutionId = 'restart-execution-id';

View File

@ -815,9 +815,9 @@ describe('WorkflowBuilderService - node type loading', () => {
let packageDir: string;
// LazyPackageDirectoryLoader lives in the externalized `n8n-core` dist and reads
// from disk through its own `fs` binding, which Vitest module mocks can't reach
// (unlike Jest's global module registry). So write a real fixture package and let
// the loader exercise the full path against the real filesystem.
// from disk through its own `fs` binding, which Vitest module mocks can't reach.
// So write a real fixture package and let the loader
// exercise the full path against the real filesystem.
beforeAll(() => {
tmpRoot = mkdtempSync(join(tmpdir(), 'n8n-ai-builder-nodes-'));
packageDir = join(tmpRoot, 'nodes-base');

View File

@ -28,9 +28,6 @@ vi.mock('@n8n/backend-common', async (importOriginal) => ({
safeJoinPath: vi.fn(),
}));
// Mock @n8n/db
// Spread the real module so transitively-imported exports resolve (Vitest throws
// on undeclared mock exports, unlike Jest), overriding only the repos under test.
vi.mock('@n8n/db', async (importOriginal) => ({
...(await importOriginal<typeof import('@n8n/db')>()),
CredentialsRepository: mock<CredentialsRepository>(),

View File

@ -234,7 +234,7 @@ describe('TaskBroker', () => {
taskType: 'taskType1',
};
vi.spyOn(taskBroker, 'acceptOffer').mockResolvedValue(); // allow Jest to exit cleanly
vi.spyOn(taskBroker, 'acceptOffer').mockResolvedValue();
taskBroker.taskRequested(request);

View File

@ -150,30 +150,30 @@ describe('Telemetry', () => {
payload.is_manual = true;
payload.success = true;
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
payload.is_manual = false;
payload.success = true;
const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00');
const execTime2 = fakeTestSystemTime('2022-01-01 13:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
payload.is_manual = true;
payload.success = false;
const execTime3 = fakeJestSystemTime('2022-01-01 14:00:00');
const execTime3 = fakeTestSystemTime('2022-01-01 14:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
payload.is_manual = false;
payload.success = false;
const execTime4 = fakeJestSystemTime('2022-01-01 15:00:00');
const execTime4 = fakeTestSystemTime('2022-01-01 15:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0);
@ -199,19 +199,19 @@ describe('Telemetry', () => {
execution_source: 'user',
};
const userManualExecTime = fakeJestSystemTime('2022-01-01 12:00:00');
const userManualExecTime = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload);
payload.execution_source = 'instance_ai';
payload.mock_data_sources = 'trigger_input';
const instanceAiMockManualExecTime = fakeJestSystemTime('2022-01-01 13:00:00');
const instanceAiMockManualExecTime = fakeTestSystemTime('2022-01-01 13:00:00');
telemetry.trackWorkflowExecution(payload);
payload.is_manual = false;
delete payload.mock_data_sources;
const instanceAiRealProdExecTime = fakeJestSystemTime('2022-01-01 14:00:00');
const instanceAiRealProdExecTime = fakeTestSystemTime('2022-01-01 14:00:00');
telemetry.trackWorkflowExecution(payload);
const execBuffer = telemetry.getCountsBuffer();
@ -240,9 +240,9 @@ describe('Telemetry', () => {
error_node_type: 'custom-nodes-base.node-type',
};
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
let execBuffer = telemetry.getCountsBuffer();
@ -253,9 +253,9 @@ describe('Telemetry', () => {
expect(execBuffer['1'].manual_error?.first).toEqual(execTime1);
payload.error_node_type = 'n8n-nodes-base.node-type';
fakeJestSystemTime('2022-01-01 13:00:00');
fakeTestSystemTime('2022-01-01 13:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
execBuffer = telemetry.getCountsBuffer();
@ -276,7 +276,7 @@ describe('Telemetry', () => {
};
// successful execution
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0);
@ -336,7 +336,7 @@ describe('Telemetry', () => {
expect(execBuffer['2'].prod_success?.first).toEqual(execTime1);
// failed execution
const execTime2 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime2 = fakeTestSystemTime('2022-01-01 12:00:00');
payload.error_node_type = 'custom-package.custom-node';
payload.success = false;
telemetry.trackWorkflowExecution(payload);
@ -393,16 +393,16 @@ describe('Telemetry', () => {
};
// Manual crashed execution
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00');
fakeTestSystemTime('2022-01-01 12:30:00');
telemetry.trackWorkflowExecution(payload);
// Production crashed execution
payload.is_manual = false;
const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00');
const execTime2 = fakeTestSystemTime('2022-01-01 13:00:00');
telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 13:30:00');
fakeTestSystemTime('2022-01-01 13:30:00');
telemetry.trackWorkflowExecution(payload);
// Should fire "Workflow execution errored" events for manual crashed executions with n8n-nodes-base
@ -439,10 +439,10 @@ describe('Telemetry', () => {
error_node_type: 'n8n-nodes-base.another-node',
};
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackWorkflowExecution(payload1);
const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00');
const execTime2 = fakeTestSystemTime('2022-01-01 13:00:00');
telemetry.trackWorkflowExecution(payload2);
// Should fire one "Workflow execution errored" event for manual crashed execution with n8n-nodes-base
@ -833,7 +833,7 @@ describe('Telemetry', () => {
});
test('should count calls per user and endpoint', () => {
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
const execTime1 = fakeTestSystemTime('2022-01-01 12:00:00');
telemetry.trackApiInvocation({
user_id: 'user1',
@ -1059,7 +1059,7 @@ describe('Telemetry', () => {
});
});
const fakeJestSystemTime = (dateTime: string | Date): Date => {
const fakeTestSystemTime = (dateTime: string | Date): Date => {
const dt = new Date(dateTime);
vi.setSystemTime(dt);
return dt;

View File

@ -4,10 +4,6 @@ import { Container } from '@n8n/di';
import { DataSource as Connection } from '@n8n/typeorm';
import nock from 'nock';
/**
* Vitest global setup. Runs once in the main process before any test file.
* Mirrors the former Jest `globalSetup`/`globalTeardown` pair.
*/
export async function setup() {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

View File

@ -27,7 +27,6 @@ function expectCredentialsMatch(
const oldSorted = [...oldCredentials].sort((a, b) => a.id.localeCompare(b.id));
const newSorted = [...newCredentials].sort((a, b) => a.id.localeCompare(b.id));
// Jest's toEqual does deep recursive comparison of all fields
expect(newSorted).toEqual(oldSorted);
}

View File

@ -380,7 +380,7 @@ export const setupTestServer = ({
// DI container after testDb.terminate() resets it. Await the close so
// pending handlers drain before the next file's beforeAll runs in
// persistent Jest workers — otherwise stale handlers call
// Container.get(Logger), construct a fresh Logger, and trip Jest's
// Container.get(Logger), construct a fresh Logger, and trip Vitest's
// "environment torn down" guard when winston is imported.
// Skip when the server never started listening (some suites bail in
// beforeAll); calling close() on a non-listening server throws

View File

@ -7,7 +7,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2023", "dom"],
"types": ["node", "vitest/globals"],
"types": ["node", "vitest/globals", "vite/client"],
"paths": {
"@/*": ["./src/*"],
"@test/*": ["./test/shared/*"],

View File

@ -7,10 +7,6 @@ import { tscEntityTransform } from './vitest.tsc-entity-transform';
import { workspaceDistExternals } from './vitest.workspace-externals';
/**
* Path aliases mirroring the `paths` entries in `tsconfig.json`, plus the
* `@n8n/mcp-apps/server` -> source mapping the former jest config used so tests
* can resolve the specifier without the package's `dist/` being built first.
*
* Note on alias order/specificity: Vite matches a string alias only when the
* import equals it or starts with `alias + '/'`, so `@` matches `@/...` but not
* `@n8n/...` or `@test/...`. The entries are therefore non-overlapping.
@ -19,9 +15,6 @@ const alias = {
'@test-integration': path.resolve(__dirname, 'test/integration/shared'),
'@test': path.resolve(__dirname, 'test/shared'),
'@n8n/mcp-apps/server': path.resolve(__dirname, '../@n8n/mcp-apps/src/server/index.ts'),
// Force the test-utils package to load from source: its `dist/` is the stale
// jest build (still imports `jest-mock-extended`), and we want the migrated
// `vitest-mock-extended` source. `workspaceDistExternals` already excludes it.
'@n8n/backend-test-utils': path.resolve(__dirname, '../@n8n/backend-test-utils/src/index.ts'),
'@': path.resolve(__dirname, 'src'),
};
@ -33,8 +26,6 @@ const alias = {
*/
export const baseConfig = mergeConfig(
createVitestConfigWithDecorators({
// The former root jest.config set `restoreMocks: true`, and a large share of
// test files silently rely on mocks being restored before each test. Keep it.
restoreMocks: true,
}),
{
@ -44,8 +35,8 @@ export const baseConfig = mergeConfig(
plugins: [workspaceDistExternals(), tscEntityTransform()],
resolve: { alias },
test: {
// Run each test file in its own forked process, matching Jest's
// process-per-file model. This is load-bearing: cli tests register service
// Run each test file in its own forked process.
// This is load-bearing: cli tests register service
// mocks into the `@n8n/di` Container (`mockInstance`), and a shared worker
// would leak that state across files.
pool: 'forks',

View File

@ -1,16 +1,6 @@
import { mergeConfig } from 'vite';
import { baseConfig } from './vitest.config.base';
/**
* Integration tests against a postgres container started via testcontainers,
* mirroring `jest.config.integration.testcontainers.js`. Migration tests are
* excluded here (they have a dedicated testcontainers config), matching the
* former `test:postgres:integration:tc` which ignored `/test/migration/`.
*
* The testcontainers global setup *replaces* the default nock global setup
* (matching the old jest config, which overrode `globalSetup`/`globalTeardown`).
*/
const integrationTcConfig = mergeConfig(baseConfig, {
test: {
include: ['test/integration/**/*.test.ts', 'src/**/*.integration.test.ts'],

View File

@ -2,10 +2,6 @@ import { mergeConfig } from 'vite';
import { baseConfig } from './vitest.config.base';
/**
* Integration test suite, mirroring the former `jest.config.integration.js`:
* the integration/migration directories plus `*.integration.test.ts` in `src`.
*/
export default mergeConfig(baseConfig, {
test: {
include: ['test/integration/**/*.test.ts', 'src/**/*.integration.test.ts'],

View File

@ -2,12 +2,6 @@ import { mergeConfig } from 'vite';
import { baseConfig } from './vitest.config.base';
/**
* Migration tests against a postgres container started via testcontainers,
* mirroring `jest.config.migration.testcontainers.js`. Runs sequentially in a
* single process; the testcontainers global setup replaces the nock global
* setup (matching the old jest config).
*/
const migrationTcConfig = mergeConfig(baseConfig, {
test: {
include: ['test/migration/**/*.test.ts'],

View File

@ -3,15 +3,14 @@ import { mergeConfig } from 'vite';
import { baseConfig } from './vitest.config.base';
/**
* Migration test suite, mirroring the former `jest.config.migration.js`.
* Migration tests share a database and perform rollbacks, so they must run
* sequentially in a single process (the equivalent of Jest's `maxWorkers: 1`).
* sequentially in a single process.
*/
export default mergeConfig(baseConfig, {
test: {
include: ['test/migration/**/*.test.ts'],
// Migration suites do heavy DB work; some files previously set a 20s timeout
// via `jest.setTimeout`. Apply it suite-wide for both tests and hooks.
// Migration suites do heavy DB work; some files previously set a 20s timeout.
// Apply it suite-wide for both tests and hooks.
testTimeout: 20_000,
hookTimeout: 20_000,
fileParallelism: false,

View File

@ -1 +0,0 @@
module.exports = require('jest-mock-extended');

View File

@ -46,7 +46,6 @@
"@n8n/vitest-config": "workspace:*",
"@vitest/coverage-v8": "catalog:",
"axios": "catalog:",
"jest-mock-extended": "^3.0.4",
"nock": "catalog:",
"reflect-metadata": "catalog:",
"vitest": "catalog:",

View File

@ -6,7 +6,6 @@ storybook-static
.storybook
.browserslistrc
jest.config.js
vite.config.ts
*.md

View File

@ -4,7 +4,6 @@
!dist
.browserslistrc
jest.config.js
postcss.config.js
vue.config.js
dist/report.html

View File

@ -36,7 +36,6 @@
"dist/**",
"eslint.config.mjs",
"tsup.config.ts",
"jest.config.js",
"vitest.config.ts",
"**/*.test.ts"
],

View File

@ -22,7 +22,7 @@
"lint:fix": "eslint src --fix",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b",
"watch": "tsc --watch",
"test": "jest --passWithNoTests"
"test": "node -e \"process.exit(0)\""
},
"bin": {
"n8n-node-dev": "./bin/n8n-node-dev"
@ -41,7 +41,9 @@
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/inquirer": "^7.0.1"
"@types/inquirer": "^7.0.1",
"vite": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
"@n8n/di": "workspace:*",

View File

@ -11,7 +11,7 @@
*
* --unit-shards: directory of downloaded unit + integration + frontend lcov
* artifacts, nested per-artifact as `<artifact>/<pkg>/coverage/lcov.info`.
* Jest and vitest write package-RELATIVE SF paths (`src/foo.ts`); we qualify
* Vitest writes package-RELATIVE SF paths (`src/foo.ts`); we qualify
* them to repo-root-relative `packages/<pkg>/src/...` (pkg derived from the
* artifact dir) so they MERGE with the already-qualified E2E lcovs instead of
* colliding under bare `src/...` keys (every package has a `src/index.ts`).

View File

@ -1,4 +0,0 @@
/** @type {import('jest').Config} */
module.exports = {
...require('../../jest.config'),
};

View File

@ -2253,12 +2253,6 @@ type EngineAction<T = unknown> = ExecuteNodeAction<T>;
* containing the results of all requested actions.
*
* @template T - The type of metadata associated with this request
*
* @todo This should use `unknown`, but jest-mock-extended will turn this into
* `Partial<unknown>` which `unknown` cannot be assigned to, which leads to a
* lot of type errors in our tests.
* The correct fix is to make a PR to jest-mock-extended and make it handle
* `unknown` special, turning it into `unknown` instead of `Partial<unknown>`.
*/
export type EngineRequest<T = object> = {
/** Array of actions that the requesting node wants the engine to fulfill */

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,6 @@ catalog:
agent-browser: 0.26.0
ai: ^6.0.197
axios: 1.18.0
babel-jest: ^29.7.0
basic-auth: 2.0.1
cache-manager: 5.2.3
callsites: 3.1.0

View File

@ -57,7 +57,7 @@
},
{
"description": "Group testing packages",
"matchPackageNames": ["*vitest*", "*playwright*", "@testing-library/**", "*jest*"],
"matchPackageNames": ["*vitest*", "*playwright*", "@testing-library/**"],
"groupName": "testing packages"
},
{

View File

@ -124,7 +124,7 @@ Then, **once**, trigger the nightly with `workflow_dispatch` and set `bootstrap_
For a **genuine cold-start** (first run after the ledger is provisioned, the read-all returns `[]`), trigger with `bootstrap_packages: '*'` instead — it acknowledges the empty ledger and skips the per-package divergence check for every entry.
Not yet covered: **jest** packages (need Stryker's jest-runner — different setup) and `@n8n/expression-runtime` (it _is_ the isolated-vm engine; blocked on the patch in DEVP-257).
Not yet covered: `@n8n/expression-runtime` (it _is_ the isolated-vm engine; blocked on the patch in DEVP-257).
## State transitions