Bruno Charest ecefbc8611
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Clean up test files and debug artifacts, add node_modules to gitignore, export DashboardManager for testing, and enhance pytest configuration with comprehensive test markers and settings
2025-12-15 08:15:49 -05:00

215 lines
5.6 KiB
JavaScript

/**
* Setup file for frontend tests with Vitest + jsdom.
*
* This file:
* - Configures the DOM environment
* - Mocks fetch API
* - Mocks WebSocket
* - Mocks localStorage
* - Provides utility functions for tests
*/
import { vi, beforeEach, afterEach } from 'vitest';
// ============================================================================
// MOCK: localStorage
// ============================================================================
const localStorageMock = (() => {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = String(value);
}),
removeItem: vi.fn((key) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
get length() {
return Object.keys(store).length;
},
key: vi.fn((i) => Object.keys(store)[i] || null),
};
})();
Object.defineProperty(global, 'localStorage', {
value: localStorageMock,
writable: true,
});
// ============================================================================
// MOCK: fetch API
// ============================================================================
/**
* Create a mock fetch response.
* @param {object} data - Response data
* @param {number} status - HTTP status code
* @returns {Response} Mock Response object
*/
export function createMockResponse(data, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
statusText: status === 200 ? 'OK' : 'Error',
json: vi.fn().mockResolvedValue(data),
text: vi.fn().mockResolvedValue(JSON.stringify(data)),
headers: new Headers({ 'Content-Type': 'application/json' }),
};
}
/**
* Setup fetch mock with default responses.
* @param {object} responses - Map of URL patterns to responses
*/
export function setupFetchMock(responses = {}) {
const defaultResponses = {
'/api/auth/status': { setup_required: false, authenticated: true, user: { username: 'test' } },
'/api/hosts': [],
'/api/tasks': [],
'/api/schedules': [],
'/api/playbooks': [],
'/api/health': { status: 'healthy' },
...responses,
};
global.fetch = vi.fn((url, options = {}) => {
const urlPath = new URL(url, 'http://localhost').pathname;
for (const [pattern, response] of Object.entries(defaultResponses)) {
if (urlPath.includes(pattern) || urlPath === pattern) {
return Promise.resolve(createMockResponse(response));
}
}
// Default 404 for unmatched URLs
return Promise.resolve(createMockResponse({ detail: 'Not found' }, 404));
});
return global.fetch;
}
// ============================================================================
// MOCK: WebSocket
// ============================================================================
export class MockWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
constructor(url) {
this.url = url;
this.readyState = MockWebSocket.CONNECTING;
this.onopen = null;
this.onclose = null;
this.onmessage = null;
this.onerror = null;
this._messageQueue = [];
// Auto-connect after a tick
setTimeout(() => {
this.readyState = MockWebSocket.OPEN;
if (this.onopen) {
this.onopen({ type: 'open' });
}
}, 0);
}
send(data) {
if (this.readyState !== MockWebSocket.OPEN) {
throw new Error('WebSocket is not open');
}
this._messageQueue.push(data);
}
close(code = 1000, reason = '') {
this.readyState = MockWebSocket.CLOSED;
if (this.onclose) {
this.onclose({ type: 'close', code, reason });
}
}
// Test helper: simulate receiving a message
_receiveMessage(data) {
if (this.onmessage) {
this.onmessage({
type: 'message',
data: typeof data === 'string' ? data : JSON.stringify(data),
});
}
}
// Test helper: simulate an error
_triggerError(error) {
if (this.onerror) {
this.onerror({ type: 'error', error });
}
}
}
global.WebSocket = MockWebSocket;
// ============================================================================
// DOM UTILITIES
// ============================================================================
/**
* Create a minimal DOM structure for testing.
*/
export function setupMinimalDOM() {
document.body.innerHTML = `
<div id="main-content">
<nav class="nav-sidebar"></nav>
<main class="main-content">
<div id="page-dashboard" class="page-section active"></div>
<div id="page-hosts" class="page-section"></div>
<div id="page-tasks" class="page-section"></div>
<div id="page-schedules" class="page-section"></div>
</main>
</div>
<div id="login-screen" class="hidden"></div>
<div id="setup-screen" class="hidden"></div>
<div id="notification-container"></div>
`;
}
/**
* Wait for DOM updates.
* @param {number} ms - Milliseconds to wait
*/
export function waitForDOM(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// ============================================================================
// HOOKS
// ============================================================================
beforeEach(() => {
// Clear localStorage
localStorageMock.clear();
// Reset fetch mock
if (global.fetch) {
global.fetch.mockClear?.();
}
// Clear DOM
document.body.innerHTML = '';
});
afterEach(() => {
vi.clearAllMocks();
});
// ============================================================================
// EXPORTS
// ============================================================================
export { localStorageMock };