test(editor): Prevent jsdom XHR leaks causing Node-22 shard-2 flake (#30732)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Declan Carroll 2026-05-19 16:42:56 +01:00 committed by GitHub
parent 04a31cdfd2
commit b136dd3de1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 51 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@ -87,6 +87,19 @@ vi.mock('vue-router', async (importOriginal) => ({
}),
}));
vi.mock('@/app/api/workflows', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/api/workflows')>();
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';

View File

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

View File

@ -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 : {};

View File

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

View File

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