From b136dd3de1ed93bdedeb0df87a33bba869d08e50 Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Tue, 19 May 2026 16:42:56 +0100 Subject: [PATCH] test(editor): Prevent jsdom XHR leaks causing Node-22 shard-2 flake (#30732) Co-authored-by: Claude Opus 4.7 --- .../editor-ui/src/__tests__/server/index.ts | 5 +++-- .../frontend/editor-ui/src/__tests__/setup.ts | 17 +++++++++++++++++ .../src/app/components/WorkflowSettings.test.ts | 2 ++ .../app/composables/useCanvasOperations.test.ts | 13 +++++++++++++ .../core/dataTable/DataTableDetailsView.test.ts | 4 ++++ .../core/dataTable/DataTableView.test.ts | 5 +++++ .../components/NodeCredentials.test.ts | 2 ++ .../credentials/views/CredentialsView.test.ts | 5 +++++ 8 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/frontend/editor-ui/src/__tests__/server/index.ts b/packages/frontend/editor-ui/src/__tests__/server/index.ts index ef8d4e7a23c..863c060d3d2 100644 --- a/packages/frontend/editor-ui/src/__tests__/server/index.ts +++ b/packages/frontend/editor-ui/src/__tests__/server/index.ts @@ -34,9 +34,10 @@ export function setupServer() { // Handle undefined endpoints server.post('/rest/:any', async () => ({})); - // Reset for everything else server.namespace = ''; - server.passthrough(); + // Intentionally no `server.passthrough()` here: in tests we never want + // mirage to fall through to the real network. Unmatched requests return + // mirage's default 404 in-memory. if (server.logging) { console.log('Mirage database'); diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index a54bfdadeb2..6ca8bcf6a87 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -413,3 +413,20 @@ Object.defineProperty(window, 'speechSynthesis', { }); loadLanguage('en', englishBaseText as unknown as LocaleMessages); + +// Block jsdom XHRs from making real network requests in tests. Unmocked store +// actions used to fire real /rest/* calls; on Node 22 the resulting dual-stack +// DNS AggregateError emits via socketErrorListener AFTER the test has finished, +// and vitest 4 promotes that to a test-run failure (~22% miss rate on shard 2). +// Short-circuiting send() means any unmocked request fails synchronously during +// the test instead of racing teardown. +XMLHttpRequest.prototype.send = function (this: XMLHttpRequest) { + Object.defineProperty(this, 'readyState', { value: 4, configurable: true }); + Object.defineProperty(this, 'status', { value: 0, configurable: true }); + Object.defineProperty(this, 'statusText', { value: '', configurable: true }); + queueMicrotask(() => { + this.dispatchEvent(new Event('readystatechange')); + this.dispatchEvent(new Event('error')); + this.dispatchEvent(new Event('loadend')); + }); +}; diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts index 877f35bcf72..ea9be78ec81 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts @@ -94,6 +94,8 @@ describe('WorkflowSettingsVue', () => { // Mock specific store actions that tests assert on workflowsStore.updateWorkflow = vi.fn(); workflowsListStore.fetchWorkflow = vi.fn(); + // Component calls this on mount; avoid a real XHR with stubActions: false. + settingsStore.getTimezones = vi.fn().mockResolvedValue({}); // Create document store on the main pinia (same one the component uses). // With stubActions: false, setSettings and getSettingsSnapshot work normally. diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index c395785078a..8347e590ff2 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -87,6 +87,19 @@ vi.mock('vue-router', async (importOriginal) => ({ }), })); +vi.mock('@/app/api/workflows', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getNewWorkflow: vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }), + }; +}); + +vi.mock('@n8n/rest-api-client/api/workflowHistory', () => ({ + getWorkflowHistory: vi.fn().mockResolvedValue([]), + getWorkflowVersion: vi.fn().mockResolvedValue({ workflow: { nodes: [], connections: {} } }), +})); + import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; import * as workflowHelpersModule from '@/app/composables/useWorkflowHelpers'; import { GRID_SIZE, PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils'; diff --git a/packages/frontend/editor-ui/src/features/core/dataTable/DataTableDetailsView.test.ts b/packages/frontend/editor-ui/src/features/core/dataTable/DataTableDetailsView.test.ts index 3833204063c..baa68d037ae 100644 --- a/packages/frontend/editor-ui/src/features/core/dataTable/DataTableDetailsView.test.ts +++ b/packages/frontend/editor-ui/src/features/core/dataTable/DataTableDetailsView.test.ts @@ -11,6 +11,10 @@ import { sourceControlEventBus } from '@/features/integrations/sourceControl.ee/ vi.mock('@/app/composables/useToast'); vi.mock('vue-router'); +vi.mock('@/app/api/workflow-dependencies', () => ({ + getResourceDependencyCounts: vi.fn().mockResolvedValue({}), + getResourceDependencies: vi.fn().mockResolvedValue({}), +})); vi.mock('@/app/composables/useDocumentTitle', () => ({ useDocumentTitle: vi.fn(() => ({ set: vi.fn(), diff --git a/packages/frontend/editor-ui/src/features/core/dataTable/DataTableView.test.ts b/packages/frontend/editor-ui/src/features/core/dataTable/DataTableView.test.ts index 55fbed102d7..0d4392bf1ed 100644 --- a/packages/frontend/editor-ui/src/features/core/dataTable/DataTableView.test.ts +++ b/packages/frontend/editor-ui/src/features/core/dataTable/DataTableView.test.ts @@ -20,6 +20,11 @@ vi.mock('@/features/collaboration/projects/composables/useProjectPages', () => ( }), })); +vi.mock('@/app/api/workflow-dependencies', () => ({ + getResourceDependencyCounts: vi.fn().mockResolvedValue({}), + getResourceDependencies: vi.fn().mockResolvedValue({}), +})); + vi.mock('@n8n/i18n', async (importOriginal) => { const actual = await importOriginal(); const actualObj = typeof actual === 'object' && actual !== null ? actual : {}; diff --git a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts index 644f8cc71d9..840274d094b 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts @@ -186,6 +186,8 @@ describe('NodeCredentials', () => { renderComponent = createComponentRenderer(NodeCredentials, defaultRenderOptions); credentialsStore = mockedStore(useCredentialsStore); + // Component triggers this on mount; avoid a real XHR with stubActions: false. + credentialsStore.fetchAllCredentials = vi.fn().mockResolvedValue([]); ndvStore = mockedStore(useNDVStore); uiStore = mockedStore(useUIStore); projectsStore = mockedStore(useProjectsStore); diff --git a/packages/frontend/editor-ui/src/features/credentials/views/CredentialsView.test.ts b/packages/frontend/editor-ui/src/features/credentials/views/CredentialsView.test.ts index cc598d2eabd..1d25968efac 100644 --- a/packages/frontend/editor-ui/src/features/credentials/views/CredentialsView.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/views/CredentialsView.test.ts @@ -25,6 +25,11 @@ vi.mock('@/features/collaboration/projects/projects.api', () => ({ getProject: vi.fn(), })); +vi.mock('@/app/api/workflow-dependencies', () => ({ + getResourceDependencyCounts: vi.fn().mockResolvedValue({}), + getResourceDependencies: vi.fn().mockResolvedValue({}), +})); + const router = createRouter({ history: createWebHistory(), routes: [