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

264 lines
8.6 KiB
TypeScript

import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* Dashboard Page Object
* Handles all interactions on the /dashboard route
* Includes The Council Chamber UI, stats, NPC chat panel, etc.
*/
export class DashboardPage extends BasePage {
// Navigation — actual link text in the nav bar
private readonly logoutButton = () => this.page.getByRole('button', { name: /leave fellowship|log out|logout|sign out/i }).first();
private readonly questsLink = () => this.page.getByRole('link', { name: /scrolls.*middle-earth|quests/i }).first();
private readonly mapLink = () => this.page.getByTestId('header-nav-map');
private readonly inventoryLink = () => this.page.getByRole('link', { name: /^inventory$/i }).first();
// Greeting — "Welcome, {user.role}! Track the Fellowship's journey"
private readonly greeting = () => this.page.getByText(/welcome,/i).first();
// Quest stat cards — use text labels (no data-testid on these elements)
private readonly totalQuestsCard = () =>
this.page.locator('div.text-center').filter({ hasText: 'Total Quest Objectives' });
private readonly completedQuestsCard = () =>
this.page.locator('div.text-center').filter({ hasText: 'It Is Done' });
// Mission briefing card HAS data-testid="mission-briefing" from Card component
private readonly missionBriefing = () => this.page.locator('[data-testid="mission-briefing"]').first();
// Recent quests — heading + h3 titles (text-lg distinguishes from CharacterPanel's text-xl h3)
private readonly recentQuestsHeading = () =>
this.page.getByText(/recent quest objectives|filtered quests/i).first();
private readonly recentQuestItems = () =>
this.page.locator('h3.font-epic.text-lg');
// Dark magic warning Alert — title text "Dark Magic Detected!"
private readonly darkMagicWarning = () =>
this.page.getByText(/dark magic detected/i).first();
// NPC chat panel — CharacterPanel renders with data-testid="character-panel"
private readonly chatPanel = () => this.page.locator('[data-testid="character-panel"]').first();
private readonly chatButton = () => this.page.getByRole('button', { name: /chat|companion|talk/i }).first();
async goto(): Promise<void> {
await super.goto('/dashboard');
}
/**
* Check if dashboard is fully loaded
* Uses active polling to verify dashboard is ready
* Handles loading states and async data
*/
async isLoaded(): Promise<boolean> {
try {
// First wait for loading state to clear
const isStillLoading = await this.page
.getByText(/opening the gates of rivendell/i)
.isVisible({ timeout: 1000 })
.catch(() => false);
if (isStillLoading) {
await this.pollUntil(
async () => {
const loading = await this.page
.getByText(/opening the gates of rivendell/i)
.isVisible()
.catch(() => false);
return !loading;
},
20000,
1000,
);
}
// Poll for mission briefing which is a primary dashboard element
await this.pollForElementVisible(
this.page.locator('[data-testid="mission-briefing"]'),
15000, // 15 second timeout
500, // Poll every 500ms
);
return true;
} catch {
return false;
}
}
/**
* Get personalized greeting text
*/
async getGreeting(): Promise<string> {
await this.page.waitForLoadState('networkidle').catch(() => {});
return this.greeting().textContent().then(t => t || '').catch(() => '');
}
/**
* Verify personalized greeting contains character name
* The dashboard shows "Welcome, {user.role}!" — match via the greeting paragraph specifically
*/
async expectGreetingForCharacter(characterName: string): Promise<void> {
// Use greeting() locator which is the <p> with greeting text, filtered to avoid strict mode violation
const greetingWithName = this.greeting().filter({ hasText: new RegExp(characterName, 'i') });
await expect(greetingWithName).toBeVisible({ timeout: 10000 });
}
/**
* Get total quest count from the stats card
* Dashboard renders <div class="text-4xl">{stats.total}</div> above label
*/
async getTotalQuestCount(): Promise<number> {
try {
await this.totalQuestsCard().waitFor({ state: 'visible', timeout: 10000 });
} catch {
return 0;
}
const text = await this.totalQuestsCard().locator('.text-4xl, [class*="text-4xl"]').textContent();
return parseInt(text?.match(/\d+/)?.[0] || '0', 10);
}
/**
* Get completed quest count from "It Is Done" stats card
*/
async getCompletedQuestCount(): Promise<number> {
try {
await this.completedQuestsCard().waitFor({ state: 'visible', timeout: 10000 });
} catch {
return 0;
}
const text = await this.completedQuestsCard().locator('.text-4xl, [class*="text-4xl"]').textContent();
return parseInt(text?.match(/\d+/)?.[0] || '0', 10);
}
/**
* Check if journey progress visualization is visible
* Maps to the Mission Briefing card (data-testid="mission-briefing") which
* always shows completion-related status on the dashboard
*/
async isJourneyProgressVisible(): Promise<boolean> {
return this.missionBriefing().isVisible({ timeout: 10000 }).catch(() => false);
}
/**
* Get journey progress percentage (completion rate from personal stats)
*/
async getJourneyProgress(): Promise<number> {
// Completion rate is shown as "{n}%" in personal journey stats
const rateText = await this.page.getByText(/%$/).first().textContent().catch(() => '0%');
return parseInt(rateText?.replace('%', '') || '0', 10);
}
/**
* Check if recent quests panel is visible (heading "Recent Quest Objectives")
*/
async isRecentQuestsPanelVisible(): Promise<boolean> {
return this.recentQuestsHeading().isVisible({ timeout: 10000 }).catch(() => false);
}
/**
* Get list of recent quest titles from h3.font-epic elements
*/
async getRecentQuestTitles(): Promise<string[]> {
try {
await this.recentQuestsHeading().waitFor({ state: 'visible', timeout: 10000 });
} catch {
return [];
}
const items = this.recentQuestItems();
const count = await items.count();
const titles: string[] = [];
for (let i = 0; i < count; i++) {
const text = await items.nth(i).textContent();
if (text) titles.push(text.trim());
}
return titles;
}
/**
* Check if dark magic warning banner is visible
* Alert renders with title "Dark Magic Detected!"
*/
async isDarkMagicWarningVisible(): Promise<boolean> {
return this.darkMagicWarning().isVisible({ timeout: 5000 }).catch(() => false);
}
/**
* Check if NPC chat panel is visible
* CharacterPanel renders data-testid="character-panel"
*/
async isChatPanelVisible(): Promise<boolean> {
return this.chatPanel().isVisible({ timeout: 8000 }).catch(() => false);
}
/**
* Open NPC chat panel
* Polls for button and panel to appear
*/
async openChatPanel(): Promise<void> {
// First check if panel is already visible
const alreadyVisible = await this.isChatPanelVisible();
if (alreadyVisible) {
return;
}
// Poll for button to be visible
try {
await this.pollForElementVisible(
this.chatButton(),
10000,
500,
);
} catch {
// Button might not exist, continue anyway
}
// Click the button
try {
await this.chatButton().click();
} catch {
// Button not found or not clickable
throw new Error('Chat button not found on dashboard. The chat panel might not be implemented.');
}
// Poll for chat panel to appear
try {
await this.pollForElementVisible(
this.chatPanel(),
10000,
500,
);
} catch {
throw new Error('Chat panel did not appear after clicking button');
}
}
/**
* Navigate to quests via "Scrolls of Middle-earth" nav link
*/
async goToQuests(): Promise<void> {
await this.questsLink().click();
await this.page.waitForURL('**/quests', { timeout: 10000 });
}
/**
* Navigate to map via "Map of Middle-earth" nav link
*/
async goToMap(): Promise<void> {
await this.mapLink().click();
await this.page.waitForURL('**/map', { timeout: 10000 });
}
/**
* Navigate to inventory via "Inventory" nav link
*/
async goToInventory(): Promise<void> {
await this.inventoryLink().click();
await this.page.waitForURL('**/inventory', { timeout: 10000 });
}
/**
* Logout
*/
async logout(): Promise<void> {
await this.logoutButton().click();
await this.page.waitForURL('**/login');
}
}