264 lines
8.6 KiB
TypeScript
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');
|
|
}
|
|
}
|