Bruno Charest 70c15c9b6f
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
Add debug mode feature flag with environment variable parsing, UI badge indicator, secret redaction utility, and enhanced terminal session management with status checks and session limit error handling
2025-12-21 17:22:36 -05:00

216 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/config': { debug_mode: false },
'/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 };