lotr-sut/tests/e2e/pages/ChatPanel.ts
Fellowship Scholar f6a5823439 init commit
2026-03-29 20:07:56 +00:00

281 lines
8.8 KiB
TypeScript

import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { ChatMessage, BargainingContext, Ware, SuggestedAction } from '../types';
/**
* Chat Panel Page Object
* Handles NPC chat interactions embedded in dashboard and other pages
*/
export class ChatPanel extends BasePage {
// Overall character panel container
private readonly characterPanel = () => this.page.locator('[data-testid="character-panel"]').first();
// Chat messages container (scoped inside character panel)
private readonly chatPanel = () => this.characterPanel().locator('[data-testid="chat-panel"]').first();
private readonly chatMessages = () => this.chatPanel().locator('[data-testid="npc-message"], [data-testid="user-message"]');
private readonly userMessages = () => this.chatPanel().locator('[data-testid="user-message"]');
private readonly npcMessages = () => this.chatPanel().locator('[data-testid="npc-message"]');
// Input and actions (scoped to outer panel, not messages container)
private readonly messageInput = () => this.characterPanel().locator('[data-testid="chat-input"]').first();
private readonly sendButton = () =>
this.characterPanel().getByRole('button', { name: /^send$/i }).first();
private readonly resetButton = () =>
this.characterPanel().getByRole('button', { name: /new opener/i }).first();
// Character selection buttons (scoped to outer panel)
private readonly characterSelector = () =>
this.characterPanel().locator('button').filter({ hasText: /frodo|sam|gandalf/i });
private readonly characterTabs = () =>
this.characterPanel().locator('button').filter({ hasText: /frodo|sam|gandalf/i });
// Suggested actions (scoped to outer panel)
private readonly suggestedAction = () =>
this.characterPanel().locator('div').filter({ hasText: /^Suggested action/ }).first();
private readonly suggestedActionButton = () =>
this.characterPanel().locator('[data-testid="suggested-action"] button').first();
constructor(page: Page, baseUrl?: string) {
super(page, baseUrl);
}
/**
* Get all chat messages as plain text array
*/
async getMessages(): Promise<string[]> {
const allMessages = this.chatMessages();
const messages: string[] = [];
const count = await allMessages.count();
for (let i = 0; i < count; i++) {
const msg = allMessages.nth(i);
const text = await msg.textContent();
if (text) {
messages.push(text.trim());
}
}
return messages;
}
/**
* Get available actions (suggested action buttons)
*/
async getAvailableActions(): Promise<string[]> {
const actions: string[] = [];
const suggestedAction = this.suggestedAction();
const isVisible = await suggestedAction.isVisible({ timeout: 2000 }).catch(() => false);
if (isVisible) {
const text = await suggestedAction.textContent();
if (text) {
actions.push(text.trim());
}
}
return actions;
}
/**
* Check if chat panel is loaded and has initial message
*/
async isLoaded(): Promise<boolean> {
try {
const charPanel = this.characterPanel();
const isVisible = await charPanel.isVisible({ timeout: 3000 });
if (!isVisible) return false;
const messages = await this.getMessages();
return messages.length > 0;
} catch {
return false;
}
}
/**
* Open chat panel if it has an open method
*/
async open(): Promise<void> {
const charPanel = this.characterPanel();
try {
await charPanel.click();
await this.page.waitForTimeout(300);
} catch {
// Panel might already be open
}
}
/**
* Check if chat panel is visible
*/
async isVisible(): Promise<boolean> {
return this.characterPanel().isVisible({ timeout: 3000 }).catch(() => false);
}
/**
* Send a message in chat
*/
async sendMessage(message: string): Promise<void> {
// Wait for the send button to be enabled (not loading) before interacting
await this.sendButton().waitFor({ state: 'visible', timeout: 15000 });
await this.sendButton().waitFor({ state: 'attached', timeout: 15000 });
// Poll until button is not disabled (isChatLoading=false)
await this.page.waitForFunction(
() => {
const btn = document.querySelector('[data-testid="send-button"]') as HTMLButtonElement | null;
return btn !== null && !btn.disabled;
},
{ timeout: 15000 }
).catch(() => {});
const initialCount = await this.npcMessages().count();
await this.messageInput().fill(message);
await this.sendButton().click();
// Wait for NPC to respond with a new message
await this.page.waitForFunction(
([selector, minCount]) => {
const els = document.querySelectorAll(selector as string);
return els.length > (minCount as number);
},
['[data-testid="npc-message"]', initialCount] as [string, number],
{ timeout: 25000 }
).catch(() => {
// Timeout waiting for response - continue anyway
});
}
/**
* Get last NPC message
* Polls for message to be rendered
*/
async getLastNPCMessage(): Promise<string> {
const messages = this.npcMessages();
try {
const count = await messages.count();
if (count === 0) {
// Poll for first message to appear
await this.pollUntil(
async () => {
const c = await this.npcMessages().count();
return c > 0;
},
10000,
500,
);
}
} catch {
// Continue anyway
}
const finalCount = await messages.count();
if (finalCount === 0) return '';
const lastMessage = messages.nth(finalCount - 1);
return lastMessage.textContent().then(t => t || '');
}
/**
* Get last user message
*/
async getLastUserMessage(): Promise<string> {
const messages = this.userMessages();
const count = await messages.count();
if (count === 0) return '';
const lastMessage = messages.nth(count - 1);
return lastMessage.textContent().then(t => t || '');
}
/**
* Get all chat messages
*/
async getAllMessages(): Promise<Array<{ role: string; text: string }>> {
const messages: Array<{ role: string; text: string }> = [];
const allMessages = this.chatMessages();
const count = await allMessages.count();
for (let i = 0; i < count; i++) {
const msg = allMessages.nth(i);
const text = await msg.textContent();
// Read the data-testid attribute to determine role
const testId = await msg.getAttribute('data-testid');
const role = testId === 'user-message' ? 'user' : 'npc';
messages.push({
role,
text: text || '',
});
}
return messages;
}
/**
* Check if suggested action is visible
*/
async isSuggestedActionVisible(): Promise<boolean> {
return this.suggestedAction().isVisible({ timeout: 3000 }).catch(() => false);
}
/**
* Get suggested action text
*/
async getSuggestedActionText(): Promise<string> {
return this.suggestedAction().textContent().then(t => t || '');
}
/**
* Click suggested action button
*/
async clickSuggestedAction(): Promise<void> {
await this.suggestedActionButton().click();
}
/**
* Select a character to chat with
* Let the UI handle conversation state - use resetNpcChat if fresh conversation needed
*/
async selectCharacter(characterName: 'frodo' | 'sam' | 'gandalf'): Promise<void> {
const tab = this.characterTabs().filter({ hasText: new RegExp(characterName, 'i') }).first();
await tab.click();
// Wait for at least one NPC message to appear (opening message / catalog)
await this.page.waitForFunction(
() => document.querySelectorAll('[data-testid="npc-message"]').length > 0,
{ timeout: 15000 }
).catch(() => {});
// Wait for send button to become enabled (isChatLoading=false)
await this.page.waitForFunction(
() => {
const btn = document.querySelector('[data-testid="send-button"]') as HTMLButtonElement | null;
return btn !== null && !btn.disabled;
},
{ timeout: 10000 }
).catch(() => {});
}
/**
* Reset conversation
*/
async resetConversation(): Promise<void> {
await this.resetButton().click();
}
/**
* Verify message contains text
*/
async expectLastNPCMessageContains(text: string | RegExp): Promise<void> {
const message = await this.getLastNPCMessage();
if (typeof text === 'string') {
expect(message).toContain(text);
} else {
expect(message).toMatch(text);
}
}
/**
* Wait for NPC to respond
* Uses active polling for response message visibility
*/
async waitForNPCResponse(timeout: number = 15000): Promise<void> {
try {
await this.pollForElementVisible(
this.npcMessages().last(),
timeout,
500,
);
} catch {
// Message may not appear, but that's ok
}
}
}