test(maintenance): fix 5s first-test timeout flake — static import + vi.hoisted mocks

Module load of maintenance.svelte.js happened inside the first test via
dynamic import and exceeded the 5000ms testTimeout under full-suite
parallel load. Move to a single hoisted static import (mock factories
now resolve vi.fn()s created via vi.hoisted) and drop a dead no-op
placeholder test. 39/39 in isolation, 3563/3563 full suite.
This commit is contained in:
2026-09-11 09:49:52 +03:00
parent d04927bda7
commit 1d48625b10

View File

@@ -7,6 +7,11 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Static import: vi.mock calls above are hoisted, so the store module receives
// the mocked dependencies. Importing at module scope keeps the one-time module
// load out of the first test's 5000ms timeout (full-suite parallel-load flake).
import { createMaintenanceStore } from '../maintenance.svelte.js';
// Mock WebSocket (external boundary) so the store never opens a real connection
// or schedules reconnect timers. Under --coverage, an unhandled WebSocket keeps
// the event loop busy and can stall concurrent test files (5000ms timeout flake).
@@ -28,29 +33,41 @@ class MockWebSocket {
}
vi.stubGlobal('WebSocket', MockWebSocket);
// Mock maintenance API
const mockMaintenanceApi = {
listEvents: vi.fn(),
listDashboardBanners: vi.fn(),
getSettings: vi.fn(),
endMaintenance: vi.fn(),
endAllMaintenance: vi.fn(),
updateSettings: vi.fn(),
};
// Mock dependencies. vi.mock factories are hoisted above this module's body,
// so every referenced vi.fn() must be created via vi.hoisted() — otherwise a
// static import of the store module hits "Cannot access ... before
// initialization" during mock factory evaluation.
const {
mockMaintenanceApi,
mockGetMaintenanceEventsWsUrl,
mockAddToast,
mockNotificationsInfo,
mockNotificationsError,
mockNotificationsSuccess,
} = vi.hoisted(() => ({
mockMaintenanceApi: {
listEvents: vi.fn(),
listDashboardBanners: vi.fn(),
getSettings: vi.fn(),
endMaintenance: vi.fn(),
endAllMaintenance: vi.fn(),
updateSettings: vi.fn(),
},
mockGetMaintenanceEventsWsUrl: vi.fn(() => 'ws://localhost/ws/maintenance/events'),
mockAddToast: vi.fn(),
mockNotificationsInfo: vi.fn(),
mockNotificationsError: vi.fn(),
mockNotificationsSuccess: vi.fn(),
}));
vi.mock('$lib/api/maintenance', () => mockMaintenanceApi);
// Mock $lib/api (for getMaintenanceEventsWsUrl)
const mockGetMaintenanceEventsWsUrl = vi.fn(() => 'ws://localhost/ws/maintenance/events');
vi.mock('$lib/api', () => ({
getMaintenanceEventsWsUrl: mockGetMaintenanceEventsWsUrl,
}));
// Mock toasts
const mockAddToast = vi.fn();
const mockNotificationsInfo = vi.fn();
const mockNotificationsError = vi.fn();
const mockNotificationsSuccess = vi.fn();
vi.mock('$lib/toasts.svelte.js', () => ({
addToast: mockAddToast,
notifications: { success: mockNotificationsSuccess, error: mockNotificationsError, info: mockNotificationsInfo, warning: vi.fn(), show: vi.fn() },
@@ -62,7 +79,6 @@ describe('maintenanceStore', () => {
});
it('should have correct initial state', async () => {
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
expect(store.activeEvents).toEqual([]);
@@ -79,7 +95,6 @@ describe('maintenanceStore', () => {
completed: [{ id: 'e2', message: 'Done' }],
});
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadEvents();
@@ -92,7 +107,6 @@ describe('maintenanceStore', () => {
it('loadEvents should handle empty active/completed', async () => {
mockMaintenanceApi.listEvents.mockResolvedValue({});
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadEvents();
@@ -103,7 +117,6 @@ describe('maintenanceStore', () => {
it('loadEvents should set error on failure', async () => {
mockMaintenanceApi.listEvents.mockRejectedValue(new Error('Connection refused'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadEvents();
@@ -115,7 +128,6 @@ describe('maintenanceStore', () => {
let resolvePromise!: (v: unknown) => void;
mockMaintenanceApi.listEvents.mockReturnValue(new Promise((r) => { resolvePromise = r; }));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
const promise = store.loadEvents();
@@ -129,7 +141,6 @@ describe('maintenanceStore', () => {
it('loadSettings should populate settings', async () => {
mockMaintenanceApi.getSettings.mockResolvedValue({ enabled: true, notify_users: true });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadSettings();
@@ -139,7 +150,6 @@ describe('maintenanceStore', () => {
it('loadSettings should keep settings null on error', async () => {
mockMaintenanceApi.getSettings.mockRejectedValue(new Error('Not found'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadSettings();
@@ -152,7 +162,6 @@ describe('maintenanceStore', () => {
{ dashboard_id: 'd2', active: false },
]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadDashboardBanners();
@@ -163,7 +172,6 @@ describe('maintenanceStore', () => {
it('loadDashboardBanners should coerce non-array response to empty array', async () => {
mockMaintenanceApi.listDashboardBanners.mockResolvedValue(null);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadDashboardBanners();
@@ -173,7 +181,6 @@ describe('maintenanceStore', () => {
it('loadDashboardBanners should handle API error gracefully', async () => {
mockMaintenanceApi.listDashboardBanners.mockRejectedValue(new Error('Server error'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadDashboardBanners();
@@ -184,7 +191,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.endMaintenance.mockResolvedValue({ task_id: 't1' });
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.endEvent('maint-1');
@@ -197,7 +203,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.endMaintenance.mockResolvedValue({});
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.endEvent('maint-2');
@@ -207,7 +212,6 @@ describe('maintenanceStore', () => {
it('endEvent should show error toast and rethrow on failure', async () => {
mockMaintenanceApi.endMaintenance.mockRejectedValue(new Error('End failed'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await expect(store.endEvent('maint-3')).rejects.toThrow('End failed');
expect(mockNotificationsError).toHaveBeenCalledWith('End failed');
@@ -217,7 +221,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.endAllMaintenance.mockResolvedValue({ task_id: 'bulk-t1' });
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.endAllEvents();
@@ -228,7 +231,6 @@ describe('maintenanceStore', () => {
it('endAllEvents should show error toast on failure', async () => {
mockMaintenanceApi.endAllMaintenance.mockRejectedValue(new Error('Bulk failed'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await expect(store.endAllEvents()).rejects.toThrow('Bulk failed');
expect(mockNotificationsError).toHaveBeenCalledWith('Bulk failed');
@@ -238,7 +240,6 @@ describe('maintenanceStore', () => {
const newSettings = { enabled: true };
mockMaintenanceApi.updateSettings.mockResolvedValue({ enabled: true, notify_users: false });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.updateSettings(newSettings);
@@ -251,7 +252,6 @@ describe('maintenanceStore', () => {
it('updateSettings should show error toast on failure', async () => {
mockMaintenanceApi.updateSettings.mockRejectedValue(new Error('Save failed'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await expect(store.updateSettings({})).rejects.toThrow('Save failed');
expect(mockNotificationsError).toHaveBeenCalledWith('Save failed');
@@ -261,7 +261,6 @@ describe('maintenanceStore', () => {
let resolvePromise!: (v: unknown) => void;
mockMaintenanceApi.updateSettings.mockReturnValue(new Promise((r) => { resolvePromise = r; }));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
const promise = store.updateSettings({ test: true });
expect(store.isLoading).toBe(true);
@@ -280,7 +279,6 @@ describe('maintenanceStore', () => {
const mockWebSocket = vi.fn();
vi.stubGlobal('WebSocket', mockWebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.init();
@@ -303,7 +301,6 @@ describe('maintenanceStore', () => {
}
vi.stubGlobal('WebSocket', MockWs as unknown as typeof WebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
@@ -322,7 +319,6 @@ describe('maintenanceStore', () => {
.mockResolvedValueOnce({ active: [{ id: 'e1' }], completed: [] })
.mockResolvedValueOnce({ active: [{ id: 'e1' }, { id: 'e2' }], completed: [{ id: 'e3' }] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadEvents();
@@ -336,7 +332,6 @@ describe('maintenanceStore', () => {
it('loadSettings should silently fail on error (no state mutation beyond console.error)', async () => {
mockMaintenanceApi.getSettings.mockRejectedValue(new Error('fail'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadSettings();
@@ -365,7 +360,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.init();
@@ -397,7 +391,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.init();
@@ -432,7 +425,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.init();
@@ -454,7 +446,6 @@ describe('maintenanceStore', () => {
}
vi.stubGlobal('WebSocket', FailingWebSocket as unknown as typeof WebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
// init will call connectWs which should catch the constructor error
@@ -483,7 +474,6 @@ describe('maintenanceStore', () => {
}
vi.stubGlobal('WebSocket', TestWebSocket as unknown as typeof WebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [{ id: 'ws-event' }], completed: [] });
@@ -516,8 +506,6 @@ describe('maintenanceStore', () => {
}
vi.stubGlobal('WebSocket', TestWebSocket as unknown as typeof WebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
@@ -550,8 +538,6 @@ describe('maintenanceStore', () => {
}
vi.stubGlobal('WebSocket', TestWebSocket as unknown as typeof WebSocket);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
@@ -597,7 +583,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.getSettings.mockResolvedValue({});
mockMaintenanceApi.listDashboardBanners.mockResolvedValue([]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.init();
expect(wsConstructorCalls).toBe(1);
@@ -623,14 +608,7 @@ describe('maintenanceStore', () => {
vi.unstubAllGlobals();
});
it('disconnectWs should be safe when no WebSocket exists', () => {
// Need fresh store without init
const { createMaintenanceStore } = import('../maintenance.svelte.js') as any;
// Actually import properly
});
it('disconnectWs should be a no-op when called without active WebSocket', async () => {
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
// No WebSocket was created — should not throw
@@ -643,7 +621,6 @@ describe('maintenanceStore', () => {
const listEventsSpy = mockMaintenanceApi.listEvents;
listEventsSpy.mockResolvedValue({ active: [{ id: 'e1' }], completed: [] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
mockMaintenanceApi.endMaintenance.mockResolvedValue({ task_id: 't1' });
@@ -657,7 +634,6 @@ describe('maintenanceStore', () => {
const listEventsSpy = mockMaintenanceApi.listEvents;
listEventsSpy.mockResolvedValue({ active: [], completed: [{ id: 'e2' }] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
mockMaintenanceApi.endAllMaintenance.mockResolvedValue({});
@@ -670,7 +646,6 @@ describe('maintenanceStore', () => {
mockMaintenanceApi.endAllMaintenance.mockResolvedValue({});
mockMaintenanceApi.listEvents.mockResolvedValue({ active: [], completed: [] });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.endAllEvents();
@@ -682,7 +657,6 @@ describe('maintenanceStore', () => {
it('loadDashboardBanners should handle non-array data from API', async () => {
mockMaintenanceApi.listDashboardBanners.mockResolvedValue({ not: 'an array' });
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadDashboardBanners();
@@ -694,7 +668,6 @@ describe('maintenanceStore', () => {
{ dashboard_id: 'd1', active: true },
]);
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
await store.loadDashboardBanners();
@@ -707,7 +680,6 @@ describe('maintenanceStore', () => {
it('updateSettings should set isLoading to false on error', async () => {
mockMaintenanceApi.updateSettings.mockRejectedValue(new Error('Fail'));
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
store.updateSettings({}).catch(() => {});
@@ -719,13 +691,11 @@ describe('maintenanceStore', () => {
// ── isEventRemoving / pendingRemovals ─────────────────────────
it('isEventRemoving returns false for unknown event', async () => {
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
expect(store.isEventRemoving('nonexistent-id')).toBe(false);
});
it('pendingRemovals getter returns current state', async () => {
const { createMaintenanceStore } = await import('../maintenance.svelte.js');
const store = createMaintenanceStore();
expect(store.pendingRemovals).toEqual({});
});