test: Migrate @n8n/decorators to Vitest (#24737)

This commit is contained in:
Declan Carroll
2026-01-23 18:59:09 +00:00
committed by GitHub
parent 122f92b169
commit 2f370f4b5f
22 changed files with 120 additions and 30 deletions
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('jest').Config} */
module.exports = {
...require('../../../jest.config'),
transform: {
'^.+\\.ts$': ['ts-jest', { isolatedModules: false }],
},
coveragePathIgnorePatterns: ['index.ts'],
};
+5 -3
View File
@@ -11,9 +11,9 @@
"lint": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "jest",
"test:unit": "jest",
"test:dev": "jest --watch"
"test": "vitest run",
"test:unit": "vitest run",
"test:dev": "vitest"
},
"main": "dist/index.js",
"module": "src/index.ts",
@@ -23,8 +23,10 @@
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@types/express": "catalog:",
"@types/lodash": "catalog:",
"vitest": "catalog:",
"zod": "catalog:"
},
"dependencies": {
@@ -200,7 +200,7 @@ describe('Redactable Decorator', () => {
});
it('should correctly apply the original method', () => {
const spy = jest.spyOn(instance, 'methodWithUser');
const spy = vi.spyOn(instance, 'methodWithUser');
const input = {
user: {
@@ -8,7 +8,7 @@ describe('@Command decorator', () => {
let commandMetadata: CommandMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
Container.reset();
commandMetadata = new CommandMetadata();
@@ -14,7 +14,7 @@ describe('@ContextEstablishmentHook decorator', () => {
let hookMetadata: ContextEstablishmentHookMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
hookMetadata = new ContextEstablishmentHookMetadata();
Container.set(ContextEstablishmentHookMetadata, hookMetadata);
@@ -8,7 +8,7 @@ describe('Args Decorators', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
controllerRegistryMetadata = new ControllerRegistryMetadata();
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
@@ -9,7 +9,7 @@ describe('@Licensed Decorator', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
controllerRegistryMetadata = new ControllerRegistryMetadata();
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
@@ -8,7 +8,7 @@ describe('@RestController Decorator', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
Container.reset();
controllerRegistryMetadata = new ControllerRegistryMetadata();
@@ -8,7 +8,7 @@ describe('@RootLevelController Decorator', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
Container.reset();
controllerRegistryMetadata = new ControllerRegistryMetadata();
@@ -9,7 +9,7 @@ describe('Route Decorators', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
controllerRegistryMetadata = new ControllerRegistryMetadata();
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
@@ -9,7 +9,7 @@ describe('Scope Decorators', () => {
let controllerRegistryMetadata: ControllerRegistryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
controllerRegistryMetadata = new ControllerRegistryMetadata();
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
@@ -11,7 +11,7 @@ describe('@CredentialResolver decorator', () => {
let resolverMetadata: CredentialResolverEntryMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
resolverMetadata = new CredentialResolverEntryMetadata();
Container.set(CredentialResolverEntryMetadata, resolverMetadata);
@@ -10,7 +10,7 @@ describe('OnLifecycleEvent', () => {
beforeEach(() => {
lifecycleMetadata = new LifecycleMetadata();
Container.set(LifecycleMetadata, lifecycleMetadata);
jest.spyOn(lifecycleMetadata, 'register');
vi.spyOn(lifecycleMetadata, 'register');
});
it('should register a method decorated with OnLifecycleEvent', () => {
@@ -8,7 +8,7 @@ describe('@BackendModule decorator', () => {
let moduleMetadata: ModuleMetadata;
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
moduleMetadata = new ModuleMetadata();
Container.set(ModuleMetadata, moduleMetadata);
@@ -35,7 +35,7 @@ beforeEach(() => {
});
it('should register methods decorated with @OnLeaderTakeover', () => {
jest.spyOn(metadata, 'register');
vi.spyOn(metadata, 'register');
@Service()
class TestService {
@@ -51,7 +51,7 @@ it('should register methods decorated with @OnLeaderTakeover', () => {
});
it('should register methods decorated with @OnLeaderStepdown', () => {
jest.spyOn(metadata, 'register');
vi.spyOn(metadata, 'register');
@Service()
class TestService {
@@ -99,8 +99,8 @@ it('should call decorated methods when events are emitted', async () => {
}
const testService = Container.get(TestService);
jest.spyOn(testService, 'handleLeaderTakeover');
jest.spyOn(testService, 'handleLeaderStepdown');
vi.spyOn(testService, 'handleLeaderTakeover');
vi.spyOn(testService, 'handleLeaderStepdown');
multiMainSetup.registerEventHandlers();
@@ -15,7 +15,7 @@ describe('@OnPubSubEvent', () => {
});
it('should register methods decorated with @OnPubSubEvent', () => {
jest.spyOn(metadata, 'register');
vi.spyOn(metadata, 'register');
@Service()
class TestService {
@@ -9,7 +9,7 @@ describe('OnShutdown', () => {
beforeEach(() => {
shutdownMetadata = new ShutdownMetadata();
Container.set(ShutdownMetadata, shutdownMetadata);
jest.spyOn(shutdownMetadata, 'register');
vi.spyOn(shutdownMetadata, 'register');
});
it('should register a methods that is decorated with OnShutdown', () => {
+1 -1
View File
@@ -2,7 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"rootDir": ".",
"types": ["node", "jest"],
"types": ["node", "vitest/globals"],
"baseUrl": "src",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
"experimentalDecorators": true,
@@ -0,0 +1,5 @@
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
export default createVitestConfigWithDecorators({
coveragePathIgnorePatterns: ['index.ts'],
});
@@ -0,0 +1,36 @@
import swc from 'unplugin-swc';
import { mergeConfig } from 'vitest/config';
import type { InlineConfig } from 'vitest/node';
import { createVitestConfig } from './node.js';
const swcPlugin = swc.vite({
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
},
target: 'es2022',
},
// Include node_modules TypeScript files (like @n8n/tournament)
exclude: [],
});
export const createVitestConfigWithDecorators = (options: InlineConfig = {}) => {
const baseConfig = createVitestConfig(options);
return mergeConfig(baseConfig, {
plugins: [swcPlugin],
esbuild: false, // Disable esbuild - it doesn't support decoratorMetadata
server: {
deps: {
// Inline all dependencies so SWC can transform them (including TypeScript in node_modules)
inline: [/.*/],
},
},
});
};
export const vitestConfigWithDecorators = createVitestConfigWithDecorators();
+8
View File
@@ -6,6 +6,9 @@
"vite": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
"unplugin-swc": "^1.5.1"
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@swc/core": "^1.10.7",
@@ -31,6 +34,11 @@
"import": "./dist/node.js",
"require": "./dist/node.js",
"types": "./dist/node.d.ts"
},
"./node-decorators": {
"import": "./dist/node-decorators.js",
"require": "./dist/node-decorators.js",
"types": "./dist/node-decorators.d.ts"
}
},
"scripts": {
+47
View File
@@ -920,12 +920,18 @@ importers:
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@n8n/vitest-config':
specifier: workspace:*
version: link:../vitest-config
'@types/express':
specifier: 'catalog:'
version: 5.0.1
'@types/lodash':
specifier: 'catalog:'
version: 4.17.17
vitest:
specifier: 'catalog:'
version: 3.1.3(@types/debug@4.1.12)(@types/node@20.19.21)(jiti@2.6.1)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3)
zod:
specifier: 3.25.67
version: 3.25.67
@@ -1638,6 +1644,10 @@ importers:
version: 3.1.3(@types/debug@4.1.12)(@types/node@20.19.21)(jiti@2.6.1)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3)
packages/@n8n/vitest-config:
dependencies:
unplugin-swc:
specifier: ^1.5.1
version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.52.4)
devDependencies:
'@n8n/typescript-config':
specifier: workspace:*
@@ -7245,6 +7255,15 @@ packages:
rollup:
optional: true
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.52.4':
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
cpu: [arm]
@@ -13921,6 +13940,10 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
local-pkg@0.5.0:
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
engines: {node: '>=14'}
@@ -17768,6 +17791,11 @@ packages:
vue-template-es2015-compiler:
optional: true
unplugin-swc@1.5.9:
resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==}
peerDependencies:
'@swc/core': ^1.2.108
unplugin@1.0.1:
resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==}
@@ -23738,6 +23766,14 @@ snapshots:
optionalDependencies:
rollup: 4.52.4
'@rollup/pluginutils@5.3.0(rollup@4.52.4)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.52.4
'@rollup/rollup-android-arm-eabi@4.52.4':
optional: true
@@ -32231,6 +32267,8 @@ snapshots:
dependencies:
uc.micro: 2.1.0
load-tsconfig@0.2.5: {}
local-pkg@0.5.0:
dependencies:
mlly: 1.8.0
@@ -36905,6 +36943,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
unplugin-swc@1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.52.4):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.4)
'@swc/core': 1.15.8(@swc/helpers@0.5.17)
load-tsconfig: 0.2.5
unplugin: 2.3.11
transitivePeerDependencies:
- rollup
unplugin@1.0.1:
dependencies:
acorn: 8.14.0