chore: Vite 8 upgrade (#27680)

This commit is contained in:
Matsu 2026-04-02 09:27:10 +03:00 committed by GitHub
parent 34894af3fa
commit efc474cc01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 2640 additions and 1649 deletions

View file

@ -21,6 +21,7 @@ describe('resolveConnection', () => {
}
beforeEach(() => {
vi.clearAllMocks();
delete process.env.N8N_URL;
delete process.env.N8N_API_KEY;
setNoConfigFile();

View file

@ -1,5 +1,6 @@
import type { CompletionResult, CompletionSource } from '@codemirror/autocomplete';
import { CompletionContext } from '@codemirror/autocomplete';
import { ensureSyntaxTree } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import type { SQLConfig } from '../src/sql';
@ -20,6 +21,7 @@ function get(doc: string, conf: SQLConfig & { explicit?: boolean } = {}) {
}),
],
});
ensureSyntaxTree(state, state.doc.length, 1e9);
const result = state.languageDataAt<CompletionSource>('autocomplete', cur)[0](
new CompletionContext(state, cur, !!conf.explicit),
);

View file

@ -35,6 +35,7 @@
"@n8n/vitest-config": "workspace:*",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/eslint": "^9.6.1",
"@types/node": "catalog:",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/rule-tester": "^8.35.0",
"@typescript-eslint/utils": "^8.35.0",

View file

@ -4,7 +4,7 @@
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"types": ["vitest/globals"],
"types": ["vitest/globals", "node"],
"esModuleInterop": true,
"module": "NodeNext",
"moduleResolution": "nodenext",

View file

@ -33,6 +33,7 @@
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@types/node": "catalog:",
"@typescript-eslint/rule-tester": "^8.35.0",
"eslint-doc-generator": "^2.2.2",
"eslint-plugin-eslint-plugin": "^7.0.0",

View file

@ -4,7 +4,7 @@
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"types": ["vitest/globals"]
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts"]
}

View file

@ -14,7 +14,8 @@ interface TmpDirFixture {
}
export const tmpdirTest = test.extend<TmpDirFixture>({
tmpdir: async ({ expect: _expect }, use) => {
// eslint-disable-next-line no-empty-pattern
tmpdir: async ({}, use) => {
const directory = await createTempDir();
const originalCwd = process.cwd();

View file

@ -12,7 +12,7 @@ export const createVitestConfig = (options: InlineConfig = {}) => {
outputFile: { junit: './junit.xml' },
coverage: {
enabled: false,
all: false,
include: ['src/**'],
provider: 'v8',
reporter: ['text-summary', 'lcov', 'html-spa'],
},
@ -29,7 +29,7 @@ export const createVitestConfig = (options: InlineConfig = {}) => {
const { coverage } = vitestConfig.test;
coverage.enabled = true;
if (process.env.CI === 'true' && coverage.provider === 'v8') {
coverage.all = true;
coverage.include = ['src/**'];
coverage.reporter = ['cobertura'];
}
}

View file

@ -17,7 +17,7 @@ export const createBaseInlineConfig = (options: InlineConfig = {}): InlineConfig
enabled: true,
provider: 'v8',
reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary',
all: true,
include: ['src/**/*'],
},
}
: {}),

View file

@ -18,9 +18,7 @@ const MOCK_FEATURE_FLAG = 'feat:sharing';
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
function makeDateWithHourOffset(offsetInHours: number): Date {
const date = new Date();
date.setHours(date.getHours() + offsetInHours);
return date;
return new Date(Date.now() + offsetInHours * 60 * 60 * 1000);
}
const licenseConfig: GlobalConfig['license'] = {

View file

@ -88,7 +88,7 @@ export class NodeTestHarness {
this.assertOutput(testData, result, nodeExecutionOrder);
if (options.customAssertions) options.customAssertions();
});
}, 20_000);
}
@Memoized

View file

@ -3,10 +3,19 @@ import { configure } from '@testing-library/vue';
configure({ testIdAttribute: 'data-test-id' });
window.ResizeObserver =
window.ResizeObserver ||
vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));
class ResizeObserverMock extends EventTarget {
constructor() {
super();
}
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
});
afterEach(() => vi.unstubAllGlobals());

View file

@ -1,6 +1,6 @@
import { defineConfig, mergeConfig, PluginOption } from 'vite';
import { resolve } from 'path';
import { renameSync, writeFileSync, readFileSync } from 'fs';
import { renameSync, writeFileSync, readFileSync, existsSync } from 'fs';
import vue from '@vitejs/plugin-vue';
import icons from 'unplugin-icons/vite';
import dts from 'vite-plugin-dts';
@ -32,7 +32,9 @@ export default mergeConfig(
const cssPath = resolve(__dirname, 'dist', 'chat.css');
const newCssPath = resolve(__dirname, 'dist', 'style.css');
try {
renameSync(cssPath, newCssPath);
if (existsSync(cssPath)) {
renameSync(cssPath, newCssPath);
}
} catch (error) {
console.error('Failed to rename chat.css file:', error);
}

View file

@ -2,6 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "bundler",
"rootDir": ".",
"outDir": "dist",
"types": ["vite/client", "vitest/globals"],

View file

@ -9,14 +9,6 @@ configure({ testIdAttribute: 'data-test-id' });
config.global.plugins = [N8nPlugin];
window.ResizeObserver =
window.ResizeObserver ||
vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));
// Globally mock is-emoji-supported
vi.mock('is-emoji-supported', () => ({
isEmojiSupported: () => true,
@ -75,8 +67,21 @@ class PatchedPointerEvent extends OriginalPointerEvent {
}
}
class ResizeObserverMock extends EventTarget {
constructor() {
super();
}
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
beforeEach(() => {
vi.stubGlobal('MouseEvent', PatchedMouseEvent);
vi.stubGlobal('PointerEvent', PatchedPointerEvent);
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
});
afterEach(() => vi.unstubAllGlobals());

View file

@ -0,0 +1,8 @@
// this test file is a placeholder since vitest requires a minimum of 2 test files to
// allow sharding to 2 shards.
describe('utils', () => {
it('succeeds', () => {
expect(true).toBe(true);
});
});

View file

@ -2,6 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "bundler",
"rootDir": ".",
"outDir": "dist",
"types": ["vite/client", "vitest/globals"],

View file

@ -0,0 +1,8 @@
// this test file is a placeholder since vitest requires a minimum of 2 test files to
// allow sharding to 2 shards.
describe('index', () => {
it('succeeds', () => {
expect(true).toBe(true);
});
});

View file

@ -2,13 +2,12 @@
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "bundler",
"outDir": "dist",
"noEmit": true,
"useUnknownInCatchVariables": false,
"types": ["vite/client", "vitest/globals"],
"isolatedModules": true,
"paths": {
"@n8n/utils/*": ["../../../@n8n/utils/src/*"]
}
"isolatedModules": true
},
"include": ["src/**/*.ts", "vite.config.ts", "tsdown.config.ts"]
}

View file

@ -2,6 +2,7 @@
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "bundler",
"rootDir": ".",
"outDir": "dist",
"types": ["vite/client", "vitest/globals"],

View file

@ -131,17 +131,18 @@
"@types/json-schema": "^7.0.15",
"@types/lodash": "catalog:",
"@types/uuid": "catalog:",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-legacy": "^8.0.0",
"@vitejs/plugin-vue": "catalog:frontend",
"@vitest/coverage-v8": "catalog:",
"browserslist-to-esbuild": "^2.1.1",
"fake-indexeddb": "^6.0.0",
"miragejs": "^0.1.48",
"sass-embedded": "catalog:",
"unplugin-icons": "catalog:frontend",
"vite": "catalog:",
"vite-plugin-istanbul": "^7.2.0",
"vite-plugin-node-polyfills": "^0.24.0",
"vite-plugin-static-copy": "2.2.0",
"vite-plugin-istanbul": "^8.0.0",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-static-copy": "4.0.0",
"vite-svg-loader": "catalog:frontend",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",

View file

@ -196,12 +196,12 @@ export function createTestWorkflowObject({
return new Workflow({
id,
name,
nodes,
connections,
nodes: Array.isArray(nodes) ? nodes : [],
connections: typeof connections === 'object' && connections !== null ? connections : {},
active,
staticData,
settings,
pinData,
staticData: typeof staticData === 'object' && staticData !== null ? staticData : {},
settings: typeof settings === 'object' && settings !== null ? settings : {},
pinData: typeof pinData === 'object' && pinData !== null ? pinData : {},
nodeTypes: rest.nodeTypes ?? nodeTypes,
});
}
@ -254,6 +254,7 @@ export function createTestNode(node: Partial<INode> = {}): INode {
export function createTestNodeProperties(data: Partial<INodeProperties> = {}): INodeProperties {
return {
displayName: 'Name',
displayOptions: undefined,
name: 'name',
type: 'string',
default: '',

View file

@ -1,5 +1,10 @@
import { createCanvasGraphNode } from '@/features/workflows/canvas/__tests__/utils';
import { createTestNode, createTestWorkflow, mockNodeTypeDescription } from '@/__tests__/mocks';
import {
createTestNode,
createTestNodeProperties,
createTestWorkflow,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore } from '@/__tests__/utils';
import { SET_NODE_TYPE } from '@/app/constants';
@ -44,22 +49,22 @@ describe('FocusSidebar', () => {
},
});
const parameter0: INodeProperties = {
const parameter0: INodeProperties = createTestNodeProperties({
displayName: 'P0',
name: 'p0',
type: 'string',
default: '',
description: '',
validateType: 'string',
};
const parameter1: INodeProperties = {
});
const parameter1: INodeProperties = createTestNodeProperties({
displayName: 'P1',
name: 'p1',
type: 'string',
default: '',
description: '',
validateType: 'string',
};
});
let experimentalNdvStore: ReturnType<typeof mockedStore<typeof useExperimentalNdvStore>>;
let focusPanelStore: ReturnType<typeof useFocusPanelStore>;

View file

@ -6,53 +6,53 @@ exports[`useFlattenSchema > flattenMultipleSchemas > should flatten node schemas
"collapsable": true,
"id": "Test Node",
"info": undefined,
"itemCount": [MockFunction spy],
"itemCount": [MockFunction],
"lastSuccessfulPreview": false,
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"preview": false,
"title": "Test Node",
"type": "header",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json }}",
"level": 0,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": "",
"preview": false,
"title": [MockFunction spy],
"title": [MockFunction],
"type": "item",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json.obj }}",
"level": 1,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj",
"preview": false,
"title": "obj",
"type": "item",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj.foo }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json.obj.foo }}",
"level": 2,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj.foo",
"preview": false,
"title": "foo",
@ -60,13 +60,13 @@ exports[`useFlattenSchema > flattenMultipleSchemas > should flatten node schemas
},
{
"collapsable": false,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj.foo.nested }}",
"icon": "type",
"id": "Test Node-{{ $('Test Node').item.json.obj.foo.nested }}",
"level": 3,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj.foo.nested",
"preview": false,
"title": "nested",
@ -77,53 +77,53 @@ exports[`useFlattenSchema > flattenMultipleSchemas > should flatten node schemas
"collapsable": true,
"id": "Test Node",
"info": undefined,
"itemCount": [MockFunction spy],
"itemCount": [MockFunction],
"lastSuccessfulPreview": false,
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"preview": false,
"title": "Test Node",
"type": "header",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json }}",
"level": 0,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": "",
"preview": false,
"title": [MockFunction spy],
"title": [MockFunction],
"type": "item",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json.obj }}",
"level": 1,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj",
"preview": false,
"title": "obj",
"type": "item",
},
{
"binaryData": [MockFunction spy],
"binaryData": [MockFunction],
"collapsable": true,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj.foo }}",
"icon": "box",
"id": "Test Node-{{ $('Test Node').item.json.obj.foo }}",
"level": 2,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj.foo",
"preview": false,
"title": "foo",
@ -131,13 +131,13 @@ exports[`useFlattenSchema > flattenMultipleSchemas > should flatten node schemas
},
{
"collapsable": false,
"depth": [MockFunction spy],
"depth": [MockFunction],
"expression": "{{ $('Test Node').item.json.obj.foo.nested }}",
"icon": "type",
"id": "Test Node-{{ $('Test Node').item.json.obj.foo.nested }}",
"level": 3,
"nodeName": "Test Node",
"nodeType": [MockFunction spy],
"nodeType": [MockFunction],
"path": ".obj.foo.nested",
"preview": false,
"title": "nested",

View file

@ -9,36 +9,33 @@ const mockRequestPermission = vi.fn();
// Store original Notification
const originalNotification = global.Notification;
const MockNotificationConstructor = vi.fn();
function setupNotificationMock(permission: NotificationPermission = 'default') {
const MockNotification = vi
.fn()
.mockImplementation((title: string, options?: NotificationOptions) => ({
title,
...options,
}));
class MockNotification extends EventTarget {
constructor(
public title: string,
public options?: NotificationOptions,
) {
super();
Object.defineProperty(MockNotification, 'permission', {
value: permission,
writable: true,
configurable: true,
});
MockNotificationConstructor(title, options);
}
Object.defineProperty(MockNotification, 'requestPermission', {
value: mockRequestPermission,
writable: true,
configurable: true,
});
static init = vi.fn();
Object.defineProperty(global, 'Notification', {
value: MockNotification,
writable: true,
configurable: true,
});
static permission = permission;
static requestPermission = mockRequestPermission;
}
vi.stubGlobal('Notification', MockNotification);
}
describe('useBrowserNotifications', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
localStorage.clear();
// Reset Notification mock with default permission
@ -287,7 +284,7 @@ describe('useBrowserNotifications', () => {
const notification = showNotification('Test Title', { body: 'Test Body' });
expect(notification).not.toBeNull();
expect(global.Notification).toHaveBeenCalledWith('Test Title', { body: 'Test Body' });
expect(MockNotificationConstructor).toHaveBeenCalledWith('Test Title', { body: 'Test Body' });
});
it('should return null when notifications are not enabled', () => {

View file

@ -22,6 +22,7 @@ import { useHistoryStore } from '@/app/stores/history.store';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import {
createTestNode,
createTestNodeProperties,
createTestWorkflow,
createTestWorkflowObject,
mockNode,
@ -3821,12 +3822,12 @@ describe('useCanvasOperations', () => {
name: type,
version,
properties: [
{
createTestNodeProperties({
displayName: 'Value',
name: 'value',
type: 'boolean',
default: true,
},
}),
],
});

View file

@ -1,12 +1,7 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach, type Mock } from 'vitest';
import { useIntersectionObserver } from './useIntersectionObserver';
import { ref } from 'vue';
interface MockIntersectionObserverConstructor {
__callback?: IntersectionObserverCallback;
new (callback: IntersectionObserverCallback): IntersectionObserver;
}
function createMockEntry(element: Element, isIntersecting: boolean): IntersectionObserverEntry {
return {
isIntersecting,
@ -19,35 +14,43 @@ function createMockEntry(element: Element, isIntersecting: boolean): Intersectio
};
}
class MockIntersectionObserver extends IntersectionObserver {
constructor(handler: IntersectionObserverCallback, options?: IntersectionObserverInit) {
super(handler, options);
this.__callback = handler;
MockIntersectionObserver._instance = this;
MockIntersectionObserver.init(handler, options);
}
static _instance: MockIntersectionObserver;
static getInstance() {
return MockIntersectionObserver._instance;
}
__callback: IntersectionObserverCallback;
static init = vi.fn();
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
describe('useIntersectionObserver()', () => {
let mockIntersectionObserver: {
observe: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
unobserve: ReturnType<typeof vi.fn>;
};
let mockCallback: ReturnType<typeof vi.fn>;
let mockCallback: Mock;
let mockRoot: Element;
let mockConstructor: MockIntersectionObserverConstructor;
let originalIntersectionObserver: typeof IntersectionObserver;
beforeEach(() => {
vi.clearAllMocks();
// Cache original IntersectionObserver
originalIntersectionObserver = global.IntersectionObserver;
// Mock IntersectionObserver
mockIntersectionObserver = {
observe: vi.fn(),
disconnect: vi.fn(),
unobserve: vi.fn(),
};
mockConstructor = vi.fn((callback) => {
// Store callback for manual triggering
mockConstructor.__callback = callback;
return mockIntersectionObserver;
}) as unknown as MockIntersectionObserverConstructor;
global.IntersectionObserver = mockConstructor as unknown as typeof IntersectionObserver;
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
mockCallback = vi.fn();
mockRoot = document.createElement('div');
@ -69,14 +72,14 @@ describe('useIntersectionObserver()', () => {
const element = document.createElement('div');
observe(element);
expect(global.IntersectionObserver).toHaveBeenCalledWith(
expect(MockIntersectionObserver.init).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
root: mockRoot,
threshold: 0.01,
}),
);
expect(mockIntersectionObserver.observe).toHaveBeenCalledWith(element);
expect(MockIntersectionObserver.getInstance().observe).toHaveBeenCalledWith(element);
});
it('executes callback when element intersects', () => {
@ -90,7 +93,7 @@ describe('useIntersectionObserver()', () => {
observe(element);
// Simulate intersection
const callback = mockConstructor.__callback;
const callback = MockIntersectionObserver.getInstance().__callback;
if (callback) {
callback([createMockEntry(element, true)], {} as IntersectionObserver);
}
@ -109,7 +112,7 @@ describe('useIntersectionObserver()', () => {
observe(element);
// Simulate no intersection
const callback = mockConstructor.__callback;
const callback = MockIntersectionObserver.getInstance().__callback;
if (callback) {
callback([createMockEntry(element, false)], {} as IntersectionObserver);
}
@ -128,12 +131,12 @@ describe('useIntersectionObserver()', () => {
observe(element);
// Simulate intersection
const callback = mockConstructor.__callback;
const callback = MockIntersectionObserver.getInstance().__callback;
if (callback) {
callback([createMockEntry(element, true)], {} as IntersectionObserver);
}
expect(mockIntersectionObserver.disconnect).toHaveBeenCalledTimes(1);
expect(MockIntersectionObserver.getInstance().disconnect).toHaveBeenCalledTimes(1);
});
it('continues observing when once is false', () => {
@ -148,12 +151,12 @@ describe('useIntersectionObserver()', () => {
observe(element);
// Simulate intersection
const callback = mockConstructor.__callback;
const callback = MockIntersectionObserver.getInstance().__callback;
if (callback) {
callback([createMockEntry(element, true)], {} as IntersectionObserver);
}
expect(mockIntersectionObserver.disconnect).not.toHaveBeenCalled();
expect(MockIntersectionObserver.getInstance().disconnect).not.toHaveBeenCalled();
});
it('uses custom threshold when provided', () => {
@ -168,7 +171,7 @@ describe('useIntersectionObserver()', () => {
const element = document.createElement('div');
observe(element);
expect(global.IntersectionObserver).toHaveBeenCalledWith(
expect(MockIntersectionObserver.init).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
threshold: customThreshold,
@ -187,7 +190,7 @@ describe('useIntersectionObserver()', () => {
const element2 = document.createElement('div');
observe(element1);
const firstObserver = mockIntersectionObserver.disconnect;
const firstObserver = MockIntersectionObserver.getInstance().disconnect;
observe(element2);
@ -203,7 +206,7 @@ describe('useIntersectionObserver()', () => {
observe(null);
expect(global.IntersectionObserver).not.toHaveBeenCalled();
expect(MockIntersectionObserver.init).not.toHaveBeenCalled();
});
it('does nothing when observing undefined element', () => {
@ -215,7 +218,7 @@ describe('useIntersectionObserver()', () => {
observe(undefined);
expect(global.IntersectionObserver).not.toHaveBeenCalled();
expect(MockIntersectionObserver.init).not.toHaveBeenCalled();
});
it('exposes disconnect method for manual cleanup', () => {
@ -230,7 +233,7 @@ describe('useIntersectionObserver()', () => {
disconnect();
expect(mockIntersectionObserver.disconnect).toHaveBeenCalledTimes(1);
expect(MockIntersectionObserver.getInstance().disconnect).toHaveBeenCalledTimes(1);
});
it('safely handles multiple disconnect calls', () => {
@ -246,6 +249,6 @@ describe('useIntersectionObserver()', () => {
disconnect();
disconnect(); // Second call should not error
expect(mockIntersectionObserver.disconnect).toHaveBeenCalledTimes(1);
expect(MockIntersectionObserver.getInstance().disconnect).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,7 +1,21 @@
/** Mocked EventSource class to help testing */
export class MockEventSource extends EventTarget {
constructor(public url: string) {
constructor(
public url: string = 'http://test.com',
...args: unknown[]
) {
super();
MockEventSource._instance = this;
MockEventSource.init(url, ...args);
}
static init = vi.fn();
static _instance: MockEventSource;
static getInstance() {
return MockEventSource._instance;
}
simulateConnectionOpen() {

View file

@ -1,13 +1,24 @@
import { WebSocketState } from '@/app/push-connection/useWebSocketClient';
import { WebSocketState, type WebSocketStateType } from '@/app/push-connection/useWebSocketClient';
/** Mocked WebSocket class to help testing */
export class MockWebSocket extends EventTarget {
readyState: number = WebSocketState.CONNECTING;
export class MockWebSocket extends WebSocket {
readyState: WebSocketStateType = WebSocketState.CONNECTING;
constructor(public url: string) {
super();
constructor(url: string) {
super(url);
MockWebSocket._instance = this;
MockWebSocket.init(url);
}
static _instance: MockWebSocket;
static getInstance() {
return MockWebSocket._instance;
}
static init = vi.fn();
simulateConnectionOpen() {
this.dispatchEvent(new Event('open'));
this.readyState = WebSocketState.OPEN;

View file

@ -3,14 +3,8 @@ import { useEventSourceClient } from '../useEventSourceClient';
import { MockEventSource } from './mockEventSource';
describe('useEventSourceClient', () => {
let mockEventSource: MockEventSource;
beforeEach(() => {
mockEventSource = new MockEventSource('http://test.com');
// @ts-expect-error - mock EventSource
global.EventSource = vi.fn(() => mockEventSource);
vi.stubGlobal('EventSource', MockEventSource);
vi.useFakeTimers();
});
@ -26,7 +20,7 @@ describe('useEventSourceClient', () => {
const { connect } = useEventSourceClient({ url, onMessage });
connect();
expect(EventSource).toHaveBeenCalledWith(url, { withCredentials: true });
expect(MockEventSource.init).toHaveBeenCalledWith(url, { withCredentials: true });
});
test('should update connection status on successful connection', () => {
@ -36,7 +30,7 @@ describe('useEventSourceClient', () => {
});
connect();
mockEventSource.simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
});
@ -46,7 +40,7 @@ describe('useEventSourceClient', () => {
const { connect } = useEventSourceClient({ url: 'http://test.com', onMessage });
connect();
mockEventSource.simulateMessageEvent('test data');
MockEventSource.getInstance().simulateMessageEvent('test data');
expect(onMessage).toHaveBeenCalledWith('test data');
});
@ -59,13 +53,13 @@ describe('useEventSourceClient', () => {
connect();
// Simulate successful connection
mockEventSource.simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
disconnect();
expect(isConnected.value).toBe(false);
expect(mockEventSource.close).toHaveBeenCalled();
expect(MockEventSource.getInstance().close).toHaveBeenCalled();
});
test('should handle connection loss', () => {
@ -74,19 +68,19 @@ describe('useEventSourceClient', () => {
onMessage: vi.fn(),
});
connect();
expect(EventSource).toHaveBeenCalledTimes(1);
expect(MockEventSource.init).toHaveBeenCalledTimes(1);
// Simulate successful connection
mockEventSource.simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
// Simulate connection loss
mockEventSource.simulateConnectionClose();
MockEventSource.getInstance().simulateConnectionClose();
expect(isConnected.value).toBe(false);
// Advance timer to trigger reconnect
vi.advanceTimersByTime(1_000);
expect(EventSource).toHaveBeenCalledTimes(2);
expect(MockEventSource.init).toHaveBeenCalledTimes(2);
});
test('sendMessage should be a noop function', () => {
@ -97,7 +91,7 @@ describe('useEventSourceClient', () => {
connect();
// Simulate successful connection
mockEventSource.simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionOpen();
const message = 'test message';
// Should not throw error and should do nothing
@ -111,18 +105,18 @@ describe('useEventSourceClient', () => {
});
connect();
mockEventSource.simulateConnectionOpen();
mockEventSource.simulateConnectionClose();
MockEventSource.getInstance().simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionClose();
// First reconnection attempt after 1 second
vi.advanceTimersByTime(1_000);
expect(EventSource).toHaveBeenCalledTimes(2);
expect(MockEventSource.init).toHaveBeenCalledTimes(2);
mockEventSource.simulateConnectionClose();
MockEventSource.getInstance().simulateConnectionClose();
// Second reconnection attempt after 2 seconds
vi.advanceTimersByTime(2_000);
expect(EventSource).toHaveBeenCalledTimes(3);
expect(MockEventSource.init).toHaveBeenCalledTimes(3);
});
test('should reset connection attempts on successful connection', () => {
@ -133,21 +127,21 @@ describe('useEventSourceClient', () => {
connect();
// First connection attempt
mockEventSource.simulateConnectionOpen();
mockEventSource.simulateConnectionClose();
MockEventSource.getInstance().simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionClose();
// First reconnection attempt
vi.advanceTimersByTime(1_000);
expect(EventSource).toHaveBeenCalledTimes(2);
expect(MockEventSource.init).toHaveBeenCalledTimes(2);
// Successful connection
mockEventSource.simulateConnectionOpen();
MockEventSource.getInstance().simulateConnectionOpen();
// Connection lost again
mockEventSource.simulateConnectionClose();
MockEventSource.getInstance().simulateConnectionClose();
// Should start with initial delay again
vi.advanceTimersByTime(1_000);
expect(EventSource).toHaveBeenCalledTimes(3);
expect(MockEventSource.init).toHaveBeenCalledTimes(3);
});
});

View file

@ -3,13 +3,8 @@ import { useWebSocketClient } from '../useWebSocketClient';
import { MockWebSocket } from './mockWebSocketClient';
describe('useWebSocketClient', () => {
let mockWebSocket: MockWebSocket;
beforeEach(() => {
mockWebSocket = new MockWebSocket('ws://test.com');
// @ts-expect-error - mock WebSocket
global.WebSocket = vi.fn(() => mockWebSocket);
vi.stubGlobal('WebSocket', MockWebSocket);
vi.useFakeTimers();
});
@ -26,7 +21,7 @@ describe('useWebSocketClient', () => {
const { connect } = useWebSocketClient({ url, onMessage });
connect();
expect(WebSocket).toHaveBeenCalledWith(url);
expect(MockWebSocket.init).toHaveBeenCalledWith(url);
});
test('should update connection status and start heartbeat on successful connection', () => {
@ -36,13 +31,15 @@ describe('useWebSocketClient', () => {
});
connect();
mockWebSocket.simulateConnectionOpen();
MockWebSocket.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
// Advance timer to trigger heartbeat
vi.advanceTimersByTime(30_000);
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: 'heartbeat' }));
expect(MockWebSocket.getInstance().send).toHaveBeenCalledWith(
JSON.stringify({ type: 'heartbeat' }),
);
});
test('should handle incoming messages', () => {
@ -50,7 +47,7 @@ describe('useWebSocketClient', () => {
const { connect } = useWebSocketClient({ url: 'ws://test.com', onMessage });
connect();
mockWebSocket.simulateMessageEvent('test data');
MockWebSocket.getInstance().simulateMessageEvent('test data');
expect(onMessage).toHaveBeenCalledWith('test data');
});
@ -63,14 +60,14 @@ describe('useWebSocketClient', () => {
connect();
// Simulate successful connection
mockWebSocket.simulateConnectionOpen();
MockWebSocket.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
disconnect();
expect(isConnected.value).toBe(false);
expect(mockWebSocket.close).toHaveBeenCalledWith(1000);
expect(MockWebSocket.getInstance().close).toHaveBeenCalledWith(1000);
});
test('should handle connection loss', () => {
@ -79,20 +76,20 @@ describe('useWebSocketClient', () => {
onMessage: vi.fn(),
});
connect();
expect(WebSocket).toHaveBeenCalledTimes(1);
expect(MockWebSocket.init).toHaveBeenCalledTimes(1);
// Simulate successful connection
mockWebSocket.simulateConnectionOpen();
MockWebSocket.getInstance().simulateConnectionOpen();
expect(isConnected.value).toBe(true);
// Simulate connection loss
mockWebSocket.simulateConnectionClose(1006);
MockWebSocket.getInstance().simulateConnectionClose(1006);
expect(isConnected.value).toBe(false);
// Advance timer to reconnect
vi.advanceTimersByTime(1_000);
expect(WebSocket).toHaveBeenCalledTimes(2);
expect(MockWebSocket.init).toHaveBeenCalledTimes(2);
});
test('should throw error when trying to send message while disconnected', () => {
@ -103,23 +100,23 @@ describe('useWebSocketClient', () => {
test('should attempt reconnection with increasing delays', () => {
const { connect } = useWebSocketClient({
url: 'http://test.com',
url: 'ws://test.com',
onMessage: vi.fn(),
});
connect();
mockWebSocket.simulateConnectionOpen();
mockWebSocket.simulateConnectionClose(1006);
MockWebSocket.getInstance().simulateConnectionOpen();
MockWebSocket.getInstance().simulateConnectionClose(1006);
// First reconnection attempt after 1 second
vi.advanceTimersByTime(1_000);
expect(WebSocket).toHaveBeenCalledTimes(2);
expect(MockWebSocket.init).toHaveBeenCalledTimes(2);
mockWebSocket.simulateConnectionClose(1006);
MockWebSocket.getInstance().simulateConnectionClose(1006);
// Second reconnection attempt after 2 seconds
vi.advanceTimersByTime(2_000);
expect(WebSocket).toHaveBeenCalledTimes(3);
expect(MockWebSocket.init).toHaveBeenCalledTimes(3);
});
test('should send message when connected', () => {
@ -130,11 +127,11 @@ describe('useWebSocketClient', () => {
connect();
// Simulate successful connection
mockWebSocket.simulateConnectionOpen();
MockWebSocket.getInstance().simulateConnectionOpen();
const message = 'test message';
sendMessage(message);
expect(mockWebSocket.send).toHaveBeenCalledWith(message);
expect(MockWebSocket.getInstance().send).toHaveBeenCalledWith(message);
});
});

View file

@ -13,7 +13,9 @@ export const WebSocketState = {
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
};
} as const;
export type WebSocketStateType = 0 | 1 | 2 | 3;
/**
* Creates a WebSocket connection to the server. Uses reconnection logic

View file

@ -31,6 +31,7 @@ describe('useNpsSurvey', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
setActivePinia(createPinia());
useSettingsStore().settings.telemetry = { enabled: true };
npsSurveyStore = useNpsSurveyStore();

View file

@ -67,6 +67,7 @@ export const nodeTypeHttpRequest = mock<INodeTypeDescription>({
displayName: 'Authentication',
name: 'authentication',
type: 'options',
displayOptions: undefined,
options: [
{ name: 'Basic Auth', value: 'basicAuth' },
{ name: 'Digest Auth', value: 'digestAuth' },

View file

@ -8,16 +8,16 @@ import * as apiUtils from '@n8n/rest-api-client';
import type { IRestApiContext } from '@n8n/rest-api-client';
import type { ChatRequest } from '@/features/ai/assistant/assistant.types';
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest';
import type { MockInstance } from 'vitest';
import type { Mock, MockInstance } from 'vitest';
vi.mock('@n8n/rest-api-client');
describe('API: ai', () => {
describe('chatWithBuilder', () => {
let mockContext: IRestApiContext;
let mockOnMessageUpdated: ReturnType<typeof vi.fn>;
let mockOnDone: ReturnType<typeof vi.fn>;
let mockOnError: ReturnType<typeof vi.fn>;
let mockOnMessageUpdated: Mock;
let mockOnDone: Mock;
let mockOnError: Mock;
let streamRequestSpy: MockInstance;
beforeEach(() => {
@ -306,9 +306,9 @@ describe('API: ai', () => {
describe('chatWithAssistant', () => {
let mockContext: IRestApiContext;
let mockOnMessageUpdated: ReturnType<typeof vi.fn>;
let mockOnDone: ReturnType<typeof vi.fn>;
let mockOnError: ReturnType<typeof vi.fn>;
let mockOnMessageUpdated: Mock;
let mockOnDone: Mock;
let mockOnError: Mock;
let streamRequestSpy: MockInstance;
beforeEach(() => {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, vi, type Mock, type MockInstance } from 'vitest';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { useBuilderStore } from './builder.store';
@ -121,9 +121,9 @@ let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
let pinia: ReturnType<typeof createTestingPinia>;
let setWorkflowNameSpy: ReturnType<typeof vi.fn>;
let getNodeTypeSpy: ReturnType<typeof vi.fn>;
let getCredentialsByTypeSpy: ReturnType<typeof vi.fn>;
let setWorkflowNameSpy: Mock;
let getNodeTypeSpy: Mock;
let getCredentialsByTypeSpy: Mock;
const apiSpy = vi.spyOn(chatAPI, 'chatWithBuilder');
@ -152,7 +152,6 @@ vi.mock('vue-router', () => ({
let workflowState: WorkflowState;
describe('AI Builder store', () => {
beforeEach(() => {
vi.clearAllMocks();
mockDocumentState = undefined;
pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
@ -2088,10 +2087,13 @@ describe('AI Builder store', () => {
});
describe('Version management and revert functionality', () => {
const mockGetAiSessions = vi.spyOn(chatAPI, 'getAiSessions');
const mockTruncateBuilderMessages = vi.spyOn(chatAPI, 'truncateBuilderMessages');
let mockGetAiSessions: MockInstance<typeof chatAPI.getAiSessions>;
let mockTruncateBuilderMessages: MockInstance<typeof chatAPI.truncateBuilderMessages>;
beforeEach(() => {
mockGetAiSessions = vi.spyOn(chatAPI, 'getAiSessions');
mockTruncateBuilderMessages = vi.spyOn(chatAPI, 'truncateBuilderMessages');
mockGetAiSessions.mockReset();
mockTruncateBuilderMessages.mockReset();
});

View file

@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { render, fireEvent, waitFor } from '@testing-library/vue';
import ChatButtons from './ChatButtons.vue';
import type { ChatHubMessageButton } from '@n8n/api-types';
@ -9,7 +9,7 @@ describe('ChatButtons', () => {
{ text: 'Reject', link: 'https://example.com/reject', type: 'secondary' },
];
let fetchMock: ReturnType<typeof vi.fn>;
let fetchMock: Mock;
const originalFetch = global.fetch;
beforeEach(() => {

View file

@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { ref, nextTick } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
@ -30,8 +30,8 @@ describe('useChatInputFocus', () => {
let mockInputRef: ReturnType<
typeof ref<{ focus: () => void; appendText: (text: string) => void } | null>
>;
let focusSpy: ReturnType<typeof vi.fn>;
let appendTextSpy: ReturnType<typeof vi.fn>;
let focusSpy: Mock;
let appendTextSpy: Mock;
function createKeyboardEvent(key: string, options: Partial<KeyboardEvent> = {}): KeyboardEvent {
return new KeyboardEvent('keydown', {

View file

@ -41,7 +41,7 @@ describe('evaluation.store.ee', () => {
let rootStoreMock: ReturnType<typeof useRootStore>;
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
setActivePinia(createPinia());
store = useEvaluationStore();
rootStoreMock = useRootStore();

View file

@ -1,4 +1,4 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { describe, test, expect, vi, beforeEach, type Mock } from 'vitest';
import { ref, computed, nextTick, type Ref } from 'vue';
import type { PushMessage } from '@n8n/api-types';
import { useEventRelay } from '../useEventRelay';
@ -49,7 +49,7 @@ function createExecState(
// ---------------------------------------------------------------------------
describe('useEventRelay', () => {
let relay: ReturnType<typeof vi.fn>;
let relay: Mock;
let workflowExecutions: Ref<Map<string, WorkflowExecutionState>>;
let activeWorkflowId: Ref<string | null>;
let bufferedEventsStore: Map<string, PushMessage[]>;

View file

@ -9,6 +9,7 @@ import { MCP_CONNECT_WORKFLOWS_MODAL_KEY } from '@/features/ai/mcpAccess/mcp.con
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { createWorkflow } from '@/features/ai/mcpAccess/mcp.test.utils';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { type Mock } from 'vitest';
vi.mock('@/app/composables/useTelemetry', () => {
const track = vi.fn();
@ -65,7 +66,7 @@ const telemetry = useTelemetry();
let pinia: ReturnType<typeof createTestingPinia>;
let mcpStore: MockedStore<typeof useMCPStore>;
let mockOnEnableMcpAccess: ReturnType<typeof vi.fn>;
let mockOnEnableMcpAccess: Mock;
describe('MCPConnectWorkflowsModal', () => {
beforeEach(() => {

View file

@ -43,6 +43,7 @@ describe('ConfirmPasswordModal', () => {
let pinia: ReturnType<typeof createPinia>;
beforeEach(() => {
vi.restoreAllMocks();
pinia = createTestingPinia({ initialState });
});

View file

@ -36,6 +36,7 @@ describe('folders.store', () => {
let rootStore: ReturnType<typeof useRootStore>;
beforeEach(() => {
vi.restoreAllMocks();
setActivePinia(createPinia());
rootStore = useRootStore();
foldersStore = useFoldersStore();

View file

@ -306,28 +306,53 @@ describe('useCredentialOAuth', () => {
};
let mockPopup: { closed: boolean; close: ReturnType<typeof vi.fn> };
let mockBroadcastChannel: {
close: ReturnType<typeof vi.fn>;
addEventListener: ReturnType<typeof vi.fn>;
postMessage: ReturnType<typeof vi.fn>;
};
class MockBroadcastChannel {
static failOauth = false;
static noopEventListener = false;
static closeCalled = false;
static __reset() {
MockBroadcastChannel.closeCalled = false;
MockBroadcastChannel.noopEventListener = false;
MockBroadcastChannel.failOauth = false;
}
close = () => {
MockBroadcastChannel.closeCalled = true;
};
addEventListener = (event: string, handler: (e: MessageEvent) => void) => {
if (MockBroadcastChannel.noopEventListener) {
return;
}
if (MockBroadcastChannel.failOauth) {
if (event === 'message') {
setTimeout(() => handler({ data: 'error' } as MessageEvent), 0);
}
} else {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
}
};
removeEventListener = vi.fn();
postMessage = vi.fn();
}
beforeEach(() => {
mockPopup = { closed: false, close: vi.fn() };
mockBroadcastChannel = {
close: vi.fn(),
addEventListener: vi.fn(),
postMessage: vi.fn(),
};
vi.stubGlobal(
'BroadcastChannel',
vi.fn().mockImplementation(() => mockBroadcastChannel),
);
vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
vi.stubGlobal('open', vi.fn().mockReturnValue(mockPopup));
});
afterEach(() => {
MockBroadcastChannel.__reset();
vi.unstubAllGlobals();
});
@ -335,15 +360,6 @@ describe('useCredentialOAuth', () => {
const credentialsStore = mockedStore(useCredentialsStore);
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
// Make the BroadcastChannel fire success immediately
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
},
);
const { authorize } = useCredentialOAuth();
const result = await authorize(mockCredential);
@ -359,14 +375,6 @@ describe('useCredentialOAuth', () => {
};
credentialsStore.oAuth1Authorize.mockResolvedValue('https://oauth1.example.com/auth');
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
},
);
const { authorize } = useCredentialOAuth();
const result = await authorize(oauth1Credential);
@ -411,13 +419,7 @@ describe('useCredentialOAuth', () => {
const credentialsStore = mockedStore(useCredentialsStore);
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'error' } as MessageEvent), 0);
}
},
);
MockBroadcastChannel.failOauth = true;
const { authorize } = useCredentialOAuth();
const result = await authorize(mockCredential);
@ -430,18 +432,10 @@ describe('useCredentialOAuth', () => {
const credentialsStore = mockedStore(useCredentialsStore);
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
},
);
const { authorize } = useCredentialOAuth();
await authorize(mockCredential);
expect(mockBroadcastChannel.close).toHaveBeenCalled();
expect(MockBroadcastChannel.closeCalled).toBeTruthy();
});
it('should return false when aborted via signal', async () => {
@ -449,9 +443,7 @@ describe('useCredentialOAuth', () => {
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
// Don't fire any BroadcastChannel message - instead simulate abort
mockBroadcastChannel.addEventListener.mockImplementation(() => {
// no-op: message handler never fires
});
MockBroadcastChannel.noopEventListener = true;
const originalAddEventListener = AbortSignal.prototype.addEventListener;
vi.spyOn(AbortSignal.prototype, 'addEventListener').mockImplementation(function (
@ -487,27 +479,33 @@ describe('useCredentialOAuth', () => {
};
let mockPopup: { closed: boolean; close: ReturnType<typeof vi.fn> };
let mockBroadcastChannel: {
close: ReturnType<typeof vi.fn>;
addEventListener: ReturnType<typeof vi.fn>;
removeEventListener: ReturnType<typeof vi.fn>;
postMessage: ReturnType<typeof vi.fn>;
};
class MockBroadcastChannel {
static failOauth = false;
close = vi.fn();
addEventListener = (event: string, handler: (e: MessageEvent) => void) => {
if (MockBroadcastChannel.failOauth) {
if (event === 'message') {
setTimeout(() => handler({ data: 'error' } as MessageEvent), 0);
}
} else {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
}
};
removeEventListener = vi.fn();
postMessage = vi.fn();
}
beforeEach(() => {
mockTrack.mockClear();
mockPopup = { closed: false, close: vi.fn() };
mockBroadcastChannel = {
close: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
postMessage: vi.fn(),
};
vi.stubGlobal(
'BroadcastChannel',
vi.fn().mockImplementation(() => mockBroadcastChannel),
);
vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
vi.stubGlobal('open', vi.fn().mockReturnValue(mockPopup));
});
@ -520,14 +518,7 @@ describe('useCredentialOAuth', () => {
credentialsStore.createNewCredential.mockResolvedValue(createdCredential);
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'success' } as MessageEvent), 0);
}
},
);
MockBroadcastChannel.failOauth = false;
return credentialsStore;
}
@ -536,13 +527,7 @@ describe('useCredentialOAuth', () => {
credentialsStore.createNewCredential.mockResolvedValue(createdCredential);
credentialsStore.oAuth2Authorize.mockResolvedValue('https://oauth.example.com/auth');
mockBroadcastChannel.addEventListener.mockImplementation(
(event: string, handler: (e: MessageEvent) => void) => {
if (event === 'message') {
setTimeout(() => handler({ data: 'error' } as MessageEvent), 0);
}
},
);
MockBroadcastChannel.failOauth = true;
return credentialsStore;
}

View file

@ -138,66 +138,62 @@ describe('GlobalExecutionsList', () => {
expect(getByTestId('execution-list-empty')).toBeInTheDocument();
});
it(
'should handle selection flow when loading more items',
async () => {
const { getByTestId, getAllByTestId, queryByTestId, rerender } = renderComponent({
props: {
executions: executionsData[0].results as ExecutionSummaryWithScopes[],
total: executionsData[0].count,
filters: {} as ExecutionFilterType,
estimated: false,
},
});
await waitAllPromises();
test('should handle selection flow when loading more items', async () => {
const { getByTestId, getAllByTestId, queryByTestId, rerender } = renderComponent({
props: {
executions: executionsData[0].results as ExecutionSummaryWithScopes[],
total: executionsData[0].count,
filters: {} as ExecutionFilterType,
estimated: false,
},
});
await waitAllPromises();
await userEvent.click(getByTestId('select-visible-executions-checkbox'));
await userEvent.click(getByTestId('select-visible-executions-checkbox'));
await retry(() => {
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(10);
});
expect(getByTestId('select-all-executions-checkbox')).toBeInTheDocument();
expect(getByTestId('selected-items-info').textContent).toContain(10);
await userEvent.click(getByTestId('load-more-button'));
await rerender({
executions: executionsData[0].results.concat(executionsData[1].results),
filteredExecutions: executionsData[0].results.concat(executionsData[1].results),
});
await waitFor(() => expect(getAllByTestId('select-execution-checkbox').length).toBe(20));
await retry(() => {
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(10);
});
expect(getByTestId('select-all-executions-checkbox')).toBeInTheDocument();
expect(getByTestId('selected-items-info').textContent).toContain(10);
await userEvent.click(getByTestId('select-all-executions-checkbox'));
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(20);
expect(getByTestId('selected-items-info').textContent).toContain(20);
await userEvent.click(getByTestId('load-more-button'));
await rerender({
executions: executionsData[0].results.concat(executionsData[1].results),
filteredExecutions: executionsData[0].results.concat(executionsData[1].results),
});
await userEvent.click(getAllByTestId('select-execution-checkbox')[2]);
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(19);
expect(getByTestId('selected-items-info').textContent).toContain(19);
expect(getByTestId('select-visible-executions-checkbox')).toBeInTheDocument();
expect(queryByTestId('select-all-executions-checkbox')).not.toBeInTheDocument();
},
{ timeout: 10000 },
);
await waitFor(() => expect(getAllByTestId('select-execution-checkbox').length).toBe(20));
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(10);
await userEvent.click(getByTestId('select-all-executions-checkbox'));
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(20);
expect(getByTestId('selected-items-info').textContent).toContain(20);
await userEvent.click(getAllByTestId('select-execution-checkbox')[2]);
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
expect(
getAllByTestId('select-execution-checkbox').filter(
(el) => el.getAttribute('data-state') === 'checked',
).length,
).toBe(19);
expect(getByTestId('selected-items-info').textContent).toContain(19);
expect(getByTestId('select-visible-executions-checkbox')).toBeInTheDocument();
expect(queryByTestId('select-all-executions-checkbox')).not.toBeInTheDocument();
}, 10000);
it('should show "retry" data when appropriate', async () => {
const retryOf = executionsData[0].results.filter((execution) => execution.retryOf);

View file

@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { nextTick } from 'vue';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@ -62,7 +62,7 @@ describe('useChatState', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let logsStore: ReturnType<typeof useLogsStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let mockRunWorkflow: ReturnType<typeof vi.fn>;
let mockRunWorkflow: Mock;
// Mock node type that mirrors the real ChatTrigger structure:
// - Multiple 'options' collections with displayOptions at the collection level

View file

@ -6,6 +6,7 @@ import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription, WorkflowParameters } from 'n8n-workflow';
import { NodeConnectionTypes, Workflow } from 'n8n-workflow';
import { nextTick } from 'vue';
import { type Mock } from 'vitest';
const nodeType: INodeTypeDescription = {
displayName: 'OpenAI',
@ -59,7 +60,7 @@ const workflow: WorkflowParameters = {
const getNodeType = vi.fn();
let mockWorkflowData = workflow;
let mockGetNodeByName = vi.fn(() => node);
let mockGetNodeByName: Mock<(name: string) => INodeUi | null> = vi.fn(() => node);
vi.mock('@/app/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({

View file

@ -44,8 +44,9 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
baseText: (key: string) => key,
}),
}));
const { saveAsMock } = vi.hoisted(() => ({ saveAsMock: vi.fn() }));
vi.mock('file-saver', () => ({
saveAs: vi.fn(),
saveAs: saveAsMock,
}));
const mockTelemetryTrack = vi.fn();
vi.mock('@/app/composables/useTelemetry', () => ({
@ -79,6 +80,7 @@ describe('useWorkflowCommands', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }));
vi.clearAllMocks();
mockWorkflow = ref(
createTestWorkflow({
@ -357,8 +359,6 @@ describe('useWorkflowCommands', () => {
describe('export commands', () => {
it('should handle download workflow', async () => {
const { saveAs } = await import('file-saver');
const { commands } = useWorkflowCommands();
const downloadCommand = commands.value.find((cmd) => cmd.id === 'download-workflow');
@ -368,7 +368,7 @@ describe('useWorkflowCommands', () => {
expect(mockTelemetryTrack).toHaveBeenCalledWith('User exported workflow', {
workflow_id: 'workflow-123',
});
expect(saveAs).toHaveBeenCalled();
expect(saveAsMock).toHaveBeenCalled();
});
});

View file

@ -20,6 +20,7 @@ import uniqBy from 'lodash/uniqBy';
beforeEach(async () => {
setActivePinia(createTestingPinia());
vi.restoreAllMocks();
vi.spyOn(utils, 'receivesNoBinaryData').mockResolvedValue(true); // hide $binary
vi.spyOn(utils, 'isSplitInBatchesAbsent').mockReturnValue(false); // show context
vi.spyOn(utils, 'hasActiveNode').mockReturnValue(true);

View file

@ -37,6 +37,40 @@ let externalSecretsStore: ReturnType<typeof useExternalSecretsStore>;
let uiStore: ReturnType<typeof useUIStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
export async function completions(docWithCursor: string, explicit = false) {
const cursorPosition = docWithCursor.indexOf('|');
const doc = docWithCursor.slice(0, cursorPosition) + docWithCursor.slice(cursorPosition + 1);
const state = EditorState.create({
doc,
selection: { anchor: cursorPosition },
extensions: [n8nLang()],
});
const context = new CompletionContext(state, cursorPosition, explicit);
for (const completionSource of state.languageDataAt<CompletionSource>(
'autocomplete',
cursorPosition,
)) {
const result = await completionSource(context);
if (isCompletionResult(result)) return result.options;
}
return null;
}
function isCompletionResult(candidate: unknown): candidate is CompletionResult {
return (
candidate !== null &&
typeof candidate === 'object' &&
'from' in candidate &&
'options' in candidate
);
}
beforeEach(async () => {
setActivePinia(createTestingPinia());
@ -44,6 +78,7 @@ beforeEach(async () => {
uiStore = useUIStore();
settingsStore = useSettingsStore();
vi.restoreAllMocks();
vi.spyOn(utils, 'receivesNoBinaryData').mockResolvedValue(true); // hide $binary
vi.spyOn(utils, 'isSplitInBatchesAbsent').mockReturnValue(false); // show context
vi.spyOn(utils, 'hasActiveNode').mockReturnValue(true);
@ -255,9 +290,7 @@ describe('Resolution-based completions', () => {
});
test('should return completions when node reference is used as a function parameter', async () => {
const initialState = { workflows: { workflow: { nodes: mockNodes } } };
setActivePinia(createTestingPinia({ initialState }));
vi.spyOn(utils, 'autocompletableNodeNames').mockReturnValue(mockNodes.map((n) => n.name));
expect(await completions('{{ new Date($(|) }}')).toHaveLength(mockNodes.length);
});
@ -848,37 +881,3 @@ describe('Resolution-based completions', () => {
});
});
});
export async function completions(docWithCursor: string, explicit = false) {
const cursorPosition = docWithCursor.indexOf('|');
const doc = docWithCursor.slice(0, cursorPosition) + docWithCursor.slice(cursorPosition + 1);
const state = EditorState.create({
doc,
selection: { anchor: cursorPosition },
extensions: [n8nLang()],
});
const context = new CompletionContext(state, cursorPosition, explicit);
for (const completionSource of state.languageDataAt<CompletionSource>(
'autocomplete',
cursorPosition,
)) {
const result = await completionSource(context);
if (isCompletionResult(result)) return result.options;
}
return null;
}
function isCompletionResult(candidate: unknown): candidate is CompletionResult {
return (
candidate !== null &&
typeof candidate === 'object' &&
'from' in candidate &&
'options' in candidate
);
}

View file

@ -6,7 +6,9 @@ import type { WorkerInitOptions } from '../types';
import { worker } from './typescript.worker';
vi.mock('@typescript/vfs');
vi.mock('typescript');
vi.mock('typescript', async (importOriginal) => {
return await importOriginal();
});
async function createWorker({
doc,

View file

@ -10,11 +10,21 @@ describe(useViewportAutoAdjust, () => {
it('should set viewport when canvas is resized', async () => {
let resizeHandler: ResizeObserverCallback = () => {};
vi.spyOn(window, 'ResizeObserver').mockImplementation((handler) => {
resizeHandler = handler;
class ResizeObserverMock extends ResizeObserver {
constructor(handler: ResizeObserverCallback) {
super(handler);
resizeHandler = handler;
}
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
return { observe() {}, disconnect() {}, unobserve() {} } as ResizeObserver;
});
const container = document.createElement('div');
Object.defineProperty(container, 'offsetWidth', {

View file

@ -24,12 +24,15 @@ const unnamedWorkflowHistoryDataFactory = (): WorkflowHistory => ({
vi.stubGlobal(
'IntersectionObserver',
vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
})),
class {
disconnect = vi.fn();
observe = vi.fn();
takeRecords = vi.fn();
unobserve = vi.fn();
},
);
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];

View file

@ -1,5 +1,5 @@
import vue from '@vitejs/plugin-vue';
import { posix as pathPosix, resolve, sep as pathSep } from 'path';
import { resolve } from 'path';
import { defineConfig, mergeConfig, type UserConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
@ -92,7 +92,7 @@ const plugins: UserConfig['plugins'] = [
nodePopularityPlugin(),
icons({
compiler: 'vue3',
autoInstall: true,
autoInstall: NODE_ENV === 'development',
}),
// Add istanbul coverage plugin for E2E tests
...(process.env.BUILD_WITH_COVERAGE === 'true'
@ -109,20 +109,20 @@ const plugins: UserConfig['plugins'] = [
viteStaticCopy({
targets: [
{
src: pathPosix.resolve('node_modules/web-tree-sitter/tree-sitter.wasm'),
dest: resolve(__dirname, 'dist'),
src: 'node_modules/web-tree-sitter/tree-sitter.wasm',
dest: 'dist',
},
{
src: pathPosix.resolve('node_modules/curlconverter/dist/tree-sitter-bash.wasm'),
dest: resolve(__dirname, 'dist'),
src: 'node_modules/curlconverter/dist/tree-sitter-bash.wasm',
dest: 'dist',
},
// wa-sqlite WASM files for OPFS database support (no cross-origin isolation needed)
{
src: pathPosix.resolve('node_modules/wa-sqlite/dist/wa-sqlite.wasm'),
src: 'node_modules/wa-sqlite/dist/wa-sqlite.wasm',
dest: 'assets',
},
{
src: pathPosix.resolve('node_modules/wa-sqlite/dist/wa-sqlite-async.wasm'),
src: 'node_modules/wa-sqlite/dist/wa-sqlite-async.wasm',
dest: 'assets',
},
],
@ -231,6 +231,7 @@ export default mergeConfig(
base: publicPath,
envPrefix: ['VUE', 'N8N_ENV_FEAT'],
css: {
preprocessorMaxWorkers: true,
preprocessorOptions: {
scss: {
additionalData: [
@ -248,9 +249,7 @@ export default mergeConfig(
},
optimizeDeps: {
exclude: ['wa-sqlite'],
esbuildOptions: {
target,
},
rolldownOptions: {},
},
worker: {
format: 'es',

View file

@ -78,9 +78,6 @@ my operation 20,000 0.04 0.20 0.05 0.10 ±0.5% 10000
|------|------------------|----------------|
| Expression Engine | `={{ }}` evaluation speed | Runs for every node parameter |
## Notes
This package pins `vitest@^3.2.0` independently from the monorepo catalog (`^3.1.3`) because CodSpeed requires vitest 3.2+.
## Tips

View file

@ -11,9 +11,9 @@
"bench:compare": "vitest bench --run --outputJson ./profiles/benchmark-results.json && node scripts/check-regression.mjs"
},
"devDependencies": {
"@codspeed/vitest-plugin": "^4.0.0",
"@codspeed/vitest-plugin": "^5.2.0",
"@n8n/expression-runtime": "workspace:*",
"vitest": "^3.2.0",
"vitest": "catalog:",
"n8n-workflow": "workspace:*"
}
}

View file

@ -36,6 +36,14 @@ import {
import { randomInt } from '../src/utils';
import { DEFAULT_EVALUATION_METRIC } from '../src/evaluation-helpers';
vi.mock('../src/node-helpers', async () => {
const actual = await vi.importActual<typeof import('../src/node-helpers')>('../src/node-helpers');
return {
...actual,
getNodeParameters: vi.fn().mockImplementation(actual.getNodeParameters),
};
});
describe('getDomainBase should return protocol plus domain', () => {
test('in valid URLs', () => {
for (const url of validUrls(numericId)) {
@ -1345,9 +1353,9 @@ describe('generateNodesGraph', () => {
});
test('should not fail on error to resolve a node parameter for sticky node type', () => {
const workflow = mock<IWorkflowBase>({ nodes: [{ type: STICKY_NODE_TYPE }] });
const workflow = mock<IWorkflowBase>({ nodes: [{ type: STICKY_NODE_TYPE }], connections: {} });
vi.spyOn(nodeHelpers, 'getNodeParameters').mockImplementationOnce(() => {
vi.mocked(nodeHelpers.getNodeParameters).mockImplementationOnce(() => {
throw new ApplicationError('Could not find property option');
});
@ -3306,7 +3314,7 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
},
});
const runData = mockRunData('Agent', new Error('Some error'));
vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
vi.mocked(nodeHelpers.getNodeParameters).mockReturnValueOnce(
mock<INodeParameters>({ model: { value: 'gpt-4-turbo' } }),
);
@ -3360,7 +3368,7 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
],
});
vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
vi.mocked(nodeHelpers.getNodeParameters).mockReturnValueOnce(
mock<INodeParameters>({ model: { value: 'gpt-4.1-mini' } }),
);
@ -3388,7 +3396,7 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
const runData = mockRunData('Agent', new Error('Some error'));
vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
vi.mocked(nodeHelpers.getNodeParameters).mockReturnValueOnce(
mock<INodeParameters>({ model: 'gpt-4' }),
);
@ -3478,7 +3486,7 @@ describe('extractLastExecutedNodeStructuredOutputErrorInfo', () => {
});
const runData = mockRunData('Agent', new Error('Some error'));
vi.spyOn(nodeHelpers, 'getNodeParameters').mockReturnValueOnce(
vi.mocked(nodeHelpers.getNodeParameters).mockReturnValueOnce(
mock<INodeParameters>({ modelName: 'gemini-1.5-pro' }),
);

View file

@ -10,7 +10,7 @@ export default defineConfig({
test: {
reporters,
outputFile,
workspace: [
projects: [
{
test: {
...sharedTestConfig,

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,6 @@ packages:
- packages/extensions/**
- packages/testing/**
catalogMode: strict
catalog:
'@azure/identity': 4.13.0
'@codemirror/autocomplete': 6.20.0
@ -51,7 +49,7 @@ catalog:
'@types/mime-types': 3.0.1
'@types/uuid': ^10.0.0
'@types/xml2js': ^0.4.14
'@vitest/coverage-v8': 3.2.4
'@vitest/coverage-v8': 4.1.1
axios: 1.13.5
basic-auth: 2.0.1
callsites: 3.1.0
@ -80,6 +78,7 @@ catalog:
picocolors: 1.0.1
reflect-metadata: 0.2.2
rimraf: 6.0.1
sass-embedded: ^1.98.0
simple-git: 3.32.3
stream-json: 1.9.1
testcontainers: ^11.13.0
@ -93,9 +92,9 @@ catalog:
tsx: ^4.19.3
typescript: 6.0.2
uuid: 10.0.0
vite: npm:rolldown-vite@latest
vite: ^8.0.2
vite-plugin-dts: ^4.5.4
vitest: ^3.1.3
vitest: ^4.1.1
vitest-mock-extended: ^3.1.0
vm2: ^3.10.5
xml2js: 0.6.2
@ -104,6 +103,8 @@ catalog:
zod-to-json-schema: 3.23.3
playwright-core: 1.58.0
catalogMode: strict
catalogs:
e2e:
'@currents/playwright': ^1.15.3
@ -124,7 +125,7 @@ catalogs:
element-plus: 2.4.3
highlight.js: 11.8.0
pinia: ^2.2.4
unplugin-icons: ^0.19.0
unplugin-icons: ^23.0.1
vite-svg-loader: 5.1.0
vue: ^3.5.13
vue-i18n: ^11.1.2