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 { 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 { 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 { 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 { // Use greeting() locator which is the

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

{stats.total}
above label */ async getTotalQuestCount(): Promise { 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 { 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 { return this.missionBriefing().isVisible({ timeout: 10000 }).catch(() => false); } /** * Get journey progress percentage (completion rate from personal stats) */ async getJourneyProgress(): Promise { // 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 { return this.recentQuestsHeading().isVisible({ timeout: 10000 }).catch(() => false); } /** * Get list of recent quest titles from h3.font-epic elements */ async getRecentQuestTitles(): Promise { 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 { return this.darkMagicWarning().isVisible({ timeout: 5000 }).catch(() => false); } /** * Check if NPC chat panel is visible * CharacterPanel renders data-testid="character-panel" */ async isChatPanelVisible(): Promise { return this.chatPanel().isVisible({ timeout: 8000 }).catch(() => false); } /** * Open NPC chat panel * Polls for button and panel to appear */ async openChatPanel(): Promise { // 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 { await this.questsLink().click(); await this.page.waitForURL('**/quests', { timeout: 10000 }); } /** * Navigate to map via "Map of Middle-earth" nav link */ async goToMap(): Promise { await this.mapLink().click(); await this.page.waitForURL('**/map', { timeout: 10000 }); } /** * Navigate to inventory via "Inventory" nav link */ async goToInventory(): Promise { await this.inventoryLink().click(); await this.page.waitForURL('**/inventory', { timeout: 10000 }); } /** * Logout */ async logout(): Promise { await this.logoutButton().click(); await this.page.waitForURL('**/login'); } }