/**
* Test Helper Utilities
*
* Common utilities and helper functions for E2E tests.
*/
import { Page, expect, Locator } from '@playwright/test';
import { QueuedRequest } from '@ask-me-mcp/askme-shared';
/**
* Wait for element to be visible with custom timeout
*/
export async function waitForVisible(locator: Locator, timeout: number = 10000): Promise<void> {
await expect(locator).toBeVisible({ timeout });
}
/**
* Wait for element to be hidden with custom timeout
*/
export async function waitForHidden(locator: Locator, timeout: number = 10000): Promise<void> {
await expect(locator).toBeHidden({ timeout });
}
/**
* Wait for text content to match
*/
export async function waitForText(locator: Locator, text: string | RegExp, timeout: number = 10000): Promise<void> {
await expect(locator).toContainText(text, { timeout });
}
/**
* Fill form field and verify the value
*/
export async function fillAndVerify(locator: Locator, value: string): Promise<void> {
await locator.fill(value);
await expect(locator).toHaveValue(value);
}
/**
* Click button and wait for response
*/
export async function clickAndWait(
button: Locator,
waitCondition: () => Promise<void>,
timeout: number = 10000
): Promise<void> {
await button.click();
await Promise.race([
waitCondition(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Wait condition not met within ${timeout}ms`)), timeout)
)
]);
}
/**
* Simulate keyboard shortcut
*/
export async function pressShortcut(page: Page, shortcut: string): Promise<void> {
await page.keyboard.press(shortcut);
}
/**
* Wait for network request to complete
*/
export async function waitForNetworkRequest(
page: Page,
urlPattern: string | RegExp,
method: string = 'POST'
): Promise<void> {
await page.waitForRequest(request =>
request.method() === method &&
(typeof urlPattern === 'string' ?
request.url().includes(urlPattern) :
urlPattern.test(request.url())
)
);
}
/**
* Wait for SSE connection to be established
*/
export async function waitForSSEConnection(page: Page, timeout: number = 30000): Promise<void> {
// Wait for EventSource to be created and connected
await page.waitForFunction(
() => {
// Check if there's an active EventSource connection
return (window as any).EventSource &&
document.querySelector('.connection-status')?.textContent?.includes('Connected');
},
{ timeout }
);
}
/**
* Mock browser notifications for testing
*/
export async function mockBrowserNotifications(page: Page): Promise<void> {
await page.addInitScript(() => {
// Mock Notification API
(window as any).Notification = class MockNotification {
static permission = 'granted';
static requestPermission = () => Promise.resolve('granted');
constructor(title: string, options?: NotificationOptions) {
console.log('Mock notification:', title, options);
}
close() {}
};
});
}
/**
* Simulate server-sent events for testing
*/
export async function simulateSSEMessage(page: Page, message: any): Promise<void> {
await page.evaluate((msg) => {
// Simulate SSE message reception
const event = new MessageEvent('message', {
data: JSON.stringify(msg)
});
// Find the EventSource instance and dispatch the event
const eventSource = (window as any).__testEventSource;
if (eventSource && eventSource.onmessage) {
eventSource.onmessage(event);
}
}, message);
}
/**
* Mock HTTP responses for testing
*/
export async function mockHTTPResponse(
page: Page,
urlPattern: string | RegExp,
response: any,
status: number = 200
): Promise<void> {
await page.route(urlPattern, route => {
route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(response)
});
});
}
/**
* Get computed style property
*/
export async function getComputedStyle(page: Page, selector: string, property: string): Promise<string> {
return await page.evaluate(
({ sel, prop }) => {
const element = document.querySelector(sel);
return element ? window.getComputedStyle(element)[prop as any] : '';
},
{ sel: selector, prop: property }
);
}
/**
* Scroll element into view
*/
export async function scrollIntoView(locator: Locator): Promise<void> {
await locator.scrollIntoViewIfNeeded();
}
/**
* Wait for animation to complete
*/
export async function waitForAnimation(page: Page, duration: number = 300): Promise<void> {
await page.waitForTimeout(duration);
}
/**
* Check if element has CSS class
*/
export async function hasClass(locator: Locator, className: string): Promise<boolean> {
const classes = await locator.getAttribute('class');
return classes?.split(' ').includes(className) || false;
}
/**
* Get element's bounding box
*/
export async function getBoundingBox(locator: Locator) {
return await locator.boundingBox();
}
/**
* Test responsive breakpoints
*/
export async function testResponsiveBreakpoints(
page: Page,
testFunction: (viewport: { width: number; height: number }) => Promise<void>
): Promise<void> {
const breakpoints = [
{ width: 375, height: 667 }, // Mobile
{ width: 768, height: 1024 }, // Tablet
{ width: 1024, height: 768 }, // Desktop small
{ width: 1200, height: 800 }, // Desktop medium
{ width: 1920, height: 1080 } // Desktop large
];
for (const viewport of breakpoints) {
await page.setViewportSize(viewport);
await testFunction(viewport);
}
}
/**
* Generate random string for testing
*/
export function randomString(length: number = 10): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Generate random number within range
*/
export function randomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Sleep/delay utility
*/
export async function sleep(ms: number): Promise<void> {
await new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry function with exponential backoff
*/
export async function retry<T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts) {
throw lastError;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
await sleep(delay);
}
}
throw lastError!;
}
/**
* Format test duration for logging
*/
export function formatDuration(startTime: number): string {
const duration = Date.now() - startTime;
return `${duration}ms`;
}
/**
* Log test step for debugging
*/
export function logTestStep(step: string, data?: any): void {
console.log(`[TEST STEP] ${step}`, data ? JSON.stringify(data, null, 2) : '');
}
/**
* Validate request data structure
*/
export function validateRequestStructure(request: any): request is QueuedRequest {
return (
typeof request === 'object' &&
request !== null &&
typeof request.id === 'string' &&
typeof request.question === 'string' &&
typeof request.status === 'string' &&
request.timestamp instanceof Date
);
}