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:
@@ -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({});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user