/**
* Page Object Model for Request Handler Component
*
* Abstracts interactions with the main request handler page and its components.
*/
import { Page, Locator, expect } from '@playwright/test';
export class RequestHandlerPage {
readonly page: Page;
readonly header: Locator;
readonly appIcon: Locator;
readonly appTitle: Locator;
readonly connectionStatus: Locator;
readonly connectionIndicator: Locator;
readonly mainContent: Locator;
readonly waitingMessage: Locator;
readonly currentRequest: Locator;
readonly requestDisplay: Locator;
readonly timeoutMessage: Locator;
readonly cancellationMessage: Locator;
constructor(page: Page) {
this.page = page;
this.header = page.locator('.handler-header');
this.appIcon = page.locator('.app-icon');
this.appTitle = page.locator('.handler-header h1');
this.connectionStatus = page.locator('.connection-status');
this.connectionIndicator = page.locator('.status-indicator');
this.mainContent = page.locator('.handler-content');
this.waitingMessage = page.locator('.waiting-message');
this.currentRequest = page.locator('.current-request');
this.requestDisplay = page.locator('app-request-display');
this.timeoutMessage = page.locator('.timeout-message');
this.cancellationMessage = page.locator('.cancellation-message');
}
/**
* Navigate to the request handler page
*/
async goto(port?: number): Promise<void> {
const url = port ? `/request-handler?port=${port}` : '/request-handler';
await this.page.goto(url);
}
/**
* Wait for the page to be fully loaded
*/
async waitForLoad(): Promise<void> {
await expect(this.header).toBeVisible();
await expect(this.appTitle).toHaveText('Ask Me MCP');
}
/**
* Wait for connection to be established
*/
async waitForConnection(timeout: number = 20000): Promise<void> {
await expect(this.connectionStatus).toContainText('Connected', { timeout });
await expect(this.connectionStatus).toHaveClass(/connected/);
}
/**
* Wait for disconnection
*/
async waitForDisconnection(): Promise<void> {
await expect(this.connectionStatus).toContainText('Disconnected');
await expect(this.connectionStatus).not.toHaveClass(/connected/);
}
/**
* Check if waiting message is displayed
*/
async isWaitingMessageVisible(): Promise<boolean> {
return await this.waitingMessage.isVisible();
}
/**
* Check if a request is currently displayed
*/
async hasCurrentRequest(): Promise<boolean> {
return await this.currentRequest.isVisible();
}
/**
* Get the current request question text
*/
async getCurrentRequestQuestion(): Promise<string> {
const questionElement = this.requestDisplay.locator('.request-question');
return await questionElement.textContent() || '';
}
/**
* Check connection status
*/
async isConnected(): Promise<boolean> {
const statusText = await this.connectionStatus.textContent();
return statusText?.includes('Connected') || false;
}
/**
* Wait for timeout message to appear
*/
async waitForTimeoutMessage(): Promise<void> {
await expect(this.timeoutMessage).toBeVisible();
await expect(this.timeoutMessage.locator('h2')).toContainText('Request Timed Out');
}
/**
* Wait for cancellation message to appear
*/
async waitForCancellationMessage(): Promise<void> {
await expect(this.cancellationMessage).toBeVisible();
await expect(this.cancellationMessage.locator('h2')).toContainText('Aborted by Client');
}
/**
* Wait for waiting message to show connected status
* This addresses race conditions between header connection status and waiting message connection info
*/
async waitForWaitingMessageConnected(timeout: number = 15000): Promise<void> {
// First ensure waiting message is visible
await expect(this.waitingMessage).toBeVisible();
// Then wait for the connection info within the waiting message to show connected
const connectionInfo = this.waitingMessage.locator('.connection-info');
await expect(connectionInfo).toContainText('Connected to server', { timeout });
// Verify the connected styling is applied
const connectedSpan = connectionInfo.locator('.status-text.connected');
await expect(connectedSpan).toBeVisible();
}
/**
* Check if blueprint background is rendered
*/
async hasBlueprintBackground(): Promise<boolean> {
const bodyStyle = await this.page.evaluate(() => {
const body = document.querySelector('body');
const computedStyle = window.getComputedStyle(body!);
return computedStyle.backgroundImage;
});
// Check for gradient pattern that indicates blueprint background
return bodyStyle.includes('gradient') || bodyStyle.includes('radial');
}
/**
* Get the request type currently displayed
*/
async getCurrentRequestType(): Promise<string | null> {
if (await this.page.locator('app-response-input').isVisible()) {
return 'single-question';
}
if (await this.page.locator('app-multiple-choice-response').isVisible()) {
return 'multiple-choice';
}
if (await this.page.locator('app-hypothesis-response').isVisible()) {
return 'hypothesis-challenge';
}
if (await this.page.locator('app-choose-next-response').isVisible()) {
return 'choose-next';
}
return null;
}
/**
* Check if app icon is displayed
*/
async isAppIconVisible(): Promise<boolean> {
return await this.appIcon.isVisible();
}
/**
* Get app title text
*/
async getAppTitle(): Promise<string> {
return await this.appTitle.textContent() || '';
}
/**
* Check if the page has proper responsive design
*/
async checkResponsiveDesign(): Promise<void> {
// Test mobile viewport
await this.page.setViewportSize({ width: 375, height: 667 });
await expect(this.header).toBeVisible();
await expect(this.mainContent).toBeVisible();
// Test desktop viewport
await this.page.setViewportSize({ width: 1200, height: 800 });
await expect(this.header).toBeVisible();
await expect(this.mainContent).toBeVisible();
}
}