241 lines
7.6 KiB
JavaScript
241 lines
7.6 KiB
JavaScript
/**
|
|
* Test helper utilities for Playwright tests
|
|
*/
|
|
|
|
/**
|
|
* Wait for Doctor UI to be fully initialized
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function waitForDoctorReady(page, options = {}) {
|
|
const timeout = Number.isFinite(options.timeout) ? options.timeout : 20000;
|
|
// Wait for the custom ready event
|
|
await page.waitForFunction(() => window.__doctorTestReady === true, null, { timeout });
|
|
|
|
// Ensure at least one tab's content has actually mounted (preact or vanilla).
|
|
await page.waitForFunction(() => {
|
|
return Boolean(
|
|
// Settings tab
|
|
document.querySelector('#doctor-settings-panel') ||
|
|
// Stats tab
|
|
document.querySelector('#doctor-statistics-panel') ||
|
|
document.querySelector('#doctor-stats-content') ||
|
|
// Chat tab (vanilla + preact)
|
|
document.querySelector('#doctor-error-context') ||
|
|
document.querySelector('#doctor-sanitization-status') ||
|
|
document.querySelector('#doctor-messages') ||
|
|
document.querySelector('.chat-messages') ||
|
|
// Error boundary fallback UI
|
|
document.querySelector('.error-boundary-container')
|
|
);
|
|
}, null, { timeout });
|
|
}
|
|
|
|
/**
|
|
* Open the Doctor sidebar if it's closed
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function openDoctorSidebar(page) {
|
|
const sidebar = page.locator('#doctor-sidebar');
|
|
const isVisible = await sidebar.isVisible().catch(() => false);
|
|
|
|
if (!isVisible) {
|
|
await page.click('#doctor-toggle-btn');
|
|
await sidebar.waitFor({ state: 'visible', timeout: 5000 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the Doctor sidebar if it's open
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function closeDoctorSidebar(page) {
|
|
const sidebar = page.locator('#doctor-sidebar');
|
|
const isVisible = await sidebar.isVisible().catch(() => false);
|
|
|
|
if (isVisible) {
|
|
await page.click('#doctor-toggle-btn');
|
|
await sidebar.waitFor({ state: 'hidden', timeout: 5000 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate to a specific tab in the Doctor sidebar
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {'errors' | 'chat' | 'settings'} tabName
|
|
*/
|
|
export async function navigateToTab(page, tabName) {
|
|
await openDoctorSidebar(page);
|
|
await page.click(`[data-tab="${tabName}"]`);
|
|
await page.waitForTimeout(200); // Wait for tab transition
|
|
}
|
|
|
|
/**
|
|
* Get the current active tab
|
|
* @param {import('@playwright/test').Page} page
|
|
* @returns {Promise<string>}
|
|
*/
|
|
export async function getActiveTab(page) {
|
|
const activeTab = await page.locator('.doctor-tab.active').getAttribute('data-tab');
|
|
return activeTab;
|
|
}
|
|
|
|
/**
|
|
* Mock an API response
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {string} endpoint - The API endpoint to mock
|
|
* @param {object} response - The response data
|
|
* @param {number} status - HTTP status code (default: 200)
|
|
*/
|
|
export async function mockApiResponse(page, endpoint, response, status = 200) {
|
|
await page.route(`**${endpoint}`, route => {
|
|
route.fulfill({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(response),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger a mock error event
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {object} errorData
|
|
*/
|
|
export async function triggerMockError(page, errorData) {
|
|
await page.evaluate((data) => {
|
|
window.__testMocks.api._triggerEvent('execution_error', data);
|
|
}, errorData);
|
|
}
|
|
|
|
/**
|
|
* Clear all localStorage data
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function clearStorage(page) {
|
|
// Use context.clearCookies() instead of page.evaluate to avoid security errors
|
|
// This works even with file:// protocol and http:// protocol
|
|
try {
|
|
await page.context().clearCookies();
|
|
} catch (e) {
|
|
// Ignore errors if cookies can't be cleared
|
|
}
|
|
|
|
// Try to clear storage, but don't fail if it's not accessible yet
|
|
try {
|
|
await page.evaluate(() => {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
});
|
|
} catch (e) {
|
|
// Storage not accessible yet, which is fine for beforeEach hooks
|
|
// It will be cleared when the page actually loads
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for UI text to be loaded (i18n)
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function waitForI18nLoaded(page) {
|
|
await page.waitForFunction(() => {
|
|
return window.app?.Doctor?.uiText && Object.keys(window.app.Doctor.uiText).length > 0;
|
|
}, { timeout: 10000 });
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5C.5: ISLAND FALLBACK TESTING HELPERS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Disable Preact before page load by setting localStorage flag.
|
|
* Must be called BEFORE navigating to the page.
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function disablePreact(page) {
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem('doctor_preact_disabled', 'true');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enable Preact (remove disable flag).
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function enablePreact(page) {
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('doctor_preact_disabled');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Block vendor files to simulate Preact load failure.
|
|
* Forces fallback to vanilla UI.
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function simulateVendorLoadFailure(page) {
|
|
await page.route('**/web/lib/**', route => route.abort());
|
|
await page.route('**/esm.sh/**', route => route.abort());
|
|
}
|
|
|
|
/**
|
|
* Assert that fallback (vanilla) Chat UI is visible.
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function assertChatFallbackUI(page) {
|
|
const { expect } = await import('@playwright/test');
|
|
|
|
// Vanilla Chat UI has these elements
|
|
const chatMessages = page.locator('#doctor-messages');
|
|
const chatInput = page.locator('#doctor-input');
|
|
const sendBtn = page.locator('#doctor-send-btn');
|
|
|
|
await expect(chatMessages).toBeVisible({ timeout: 5000 });
|
|
await expect(chatInput).toBeVisible();
|
|
await expect(sendBtn).toBeVisible();
|
|
}
|
|
|
|
/**
|
|
* Assert that fallback (vanilla) Stats UI is visible.
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
export async function assertStatsFallbackUI(page) {
|
|
const { expect } = await import('@playwright/test');
|
|
|
|
// Vanilla Stats UI has these elements
|
|
const statsPanel = page.locator('#doctor-statistics-panel, #doctor-stats-content');
|
|
await expect(statsPanel.first()).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Assert that Preact island is active (not vanilla fallback).
|
|
* Checks for island-specific markers.
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {'chat' | 'stats'} islandType
|
|
*/
|
|
export async function assertIslandActive(page, islandType) {
|
|
const isActive = await page.evaluate((type) => {
|
|
const doctorUI = window.app?.Doctor;
|
|
if (!doctorUI) return false;
|
|
return type === 'chat' ? doctorUI.chatIslandActive : doctorUI.statsIslandActive;
|
|
}, islandType);
|
|
|
|
const { expect } = await import('@playwright/test');
|
|
expect(isActive).toBe(true);
|
|
}
|
|
|
|
/**
|
|
* Get island registry errors (for testing error boundary).
|
|
* @param {import('@playwright/test').Page} page
|
|
* @returns {Promise<Array<{id: string, timestamp: number}>>}
|
|
*/
|
|
export async function getIslandErrors(page) {
|
|
return await page.evaluate(async () => {
|
|
try {
|
|
const registry = await import('/web/island_registry.js');
|
|
return registry.getErrors().map(e => ({ id: e.id, timestamp: e.timestamp }));
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
}
|