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

282 lines
8.1 KiB
TypeScript

import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* Map Page Object
* Handles all interactions on the /map route
*/
export class MapPage extends BasePage {
// Map container
private readonly mapContainer = () => this.page.locator('[data-testid="map-container"]').first();
private readonly mapElement = () => this.page.locator('.leaflet-map').first();
// Markers
private readonly questMarkers = () => this.page.locator('[data-testid="quest-marker"]');
private readonly locationMarkers = () => this.page.locator('.location-marker');
private readonly sellerMarkers = () => this.page.locator('[data-testid="seller-marker"]');
// Popups
private readonly markerPopup = () => this.page.locator('[data-testid="marker-popup"]').first();
private readonly popupTitle = () => this.markerPopup().locator('h4').first();
private readonly popupDescription = () => this.markerPopup().locator('p').first();
private readonly popupLocation = () => this.markerPopup().locator('[data-testid="popup-location"]').first();
private readonly popupActionButton = () =>
this.page.locator('[data-testid="popup-action-button"]').first();
// Location info
private readonly locationInfo = () => this.page.locator('[data-testid="location-info"]').first();
async goto(): Promise<void> {
await super.goto('/map');
}
/**
* Check if map is loaded
* Uses active polling to detect map readiness
* Fallback checks for loading state clearing
*/
async isLoaded(): Promise<boolean> {
try {
// First check if loading indicator is gone
const isStillLoading = await this.page
.getByText(/charting the map/i)
.isVisible({ timeout: 1000 })
.catch(() => false);
if (isStillLoading) {
// Page is still loading, wait for loading to finish
await this.pollUntil(
async () => {
const loading = await this.page
.getByText(/charting the map/i)
.isVisible()
.catch(() => false);
return !loading; // Return true when loading finishes
},
20000, // 20 second timeout for loading
1000, // Poll every 1s
);
}
// Now poll for map container to be visible
await this.pollForElementVisible(
this.mapContainer(),
15000, // 15 second timeout
500, // Poll every 500ms
);
return true;
} catch {
return false;
}
}
/**
* Get count of quest markers
* Polls for markers to be rendered
*/
async getQuestMarkerCount(): Promise<number> {
try {
// Wait for at least one quest marker to appear (polls until count > 0)
await this.pollUntil(
async () => {
const count = await this.questMarkers().count();
return count > 0;
},
15000, // 15s — markers render after map tiles load
500,
);
} catch {
// Timeout: markers may genuinely be 0, fall through and return actual count
}
return this.questMarkers().count();
}
/**
* Get count of location markers
* Polls for markers to be rendered
*/
async getLocationMarkerCount(): Promise<number> {
try {
await this.pollUntil(
async () => {
const count = await this.locationMarkers().count();
return count >= 0; // Always true, but waits a bit for markers to render
},
5000,
500,
);
} catch {
// Timeout, continue
}
return this.locationMarkers().count();
}
/**
* Get count of seller markers
* Polls for markers to be rendered
*/
async getSellerMarkerCount(): Promise<number> {
try {
await this.pollUntil(
async () => {
const count = await this.sellerMarkers().count();
return count >= 0; // Always true, but waits a bit for markers to render
},
5000,
500,
);
} catch {
// Timeout, continue
}
return this.sellerMarkers().count();
}
/**
* Click on a quest marker by index
*/
async clickQuestMarker(index: number = 0): Promise<void> {
const markers = this.questMarkers();
await markers.nth(index).click();
// Wait for popup or quest-details-card to appear (either indicates a successful click)
await Promise.race([
this.markerPopup().waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}),
this.page.locator('.quest-details-card').waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}),
]);
}
/**
* Click on a location marker by index
*/
async clickLocationMarker(index: number = 0): Promise<void> {
const markers = this.locationMarkers();
await markers.nth(index).click();
await this.markerPopup().waitFor({ state: 'visible', timeout: 5000 });
}
/**
* Click on a seller marker by index
* Sellers are character markers (Frodo, Sam, Gandalf) who sell items
*/
async clickSellerMarker(index: number = 0): Promise<void> {
try {
// Poll for seller markers to appear
await this.pollUntil(
async () => {
const count = await this.sellerMarkers().count();
return count > index; // Wait for at least index+1 markers
},
10000,
500,
);
} catch {
// Markers might not load, continue anyway
}
const markers = this.sellerMarkers();
// Click the marker
try {
await markers.nth(index).click({ timeout: 5000 });
// Give the map time to process the click and Leaflet to open the popup
await this.page.waitForTimeout(1000);
} catch (err) {
// Marker might not be clickable directly, try with JS
await this.page.evaluate((idx: number) => {
const selector = '[data-testid="seller-marker"]';
const elements = document.querySelectorAll(selector);
if (elements[idx]) {
(elements[idx] as HTMLElement).click();
}
}, index);
// Give the map time to process the click and Leaflet to open the popup
await this.page.waitForTimeout(1000);
}
// Wait for popup to appear (may not appear if user is not logged in and navigation occurs)
try {
await this.pollForElementVisible(
this.markerPopup(),
3000,
200,
);
} catch {
// Popup may not appear if clicking triggers navigation (e.g. to /login)
// This is acceptable — the step verification handles the outcome
}
}
/**
* Check if marker popup is visible
*/
async isPopupVisible(): Promise<boolean> {
return this.markerPopup().isVisible({ timeout: 3000 }).catch(() => false);
}
/**
* Get popup title
*/
async getPopupTitle(): Promise<string> {
return this.popupTitle().textContent().then(t => t || '');
}
/**
* Get popup description
*/
async getPopupDescription(): Promise<string> {
return this.popupDescription().textContent().then(t => t || '');
}
/**
* Get popup location
*/
async getPopupLocation(): Promise<string> {
return this.popupLocation().textContent().then(t => t || '');
}
/**
* Click popup action button
* For sellers, this opens the bargaining interface
*/
async clickPopupActionButton(): Promise<void> {
try {
// Poll for button to be visible
await this.pollForElementVisible(
this.popupActionButton(),
10000,
500,
);
} catch {
// Button might not be visible, try clicking anyway
}
try {
await this.popupActionButton().click();
} catch (err) {
// Try with JavaScript if regular click fails
await this.page.evaluate(() => {
const btn = document.querySelector('[data-testid="popup-action-button"]') as HTMLElement;
if (btn) {
btn.click();
}
});
}
// Wait a moment for the bargaining interface to open
await this.page.waitForTimeout(1000);
}
/**
* Verify popup contains quest information
*/
async expectPopupHasQuestInfo(questTitle: string): Promise<void> {
await expect(this.popupTitle()).toContainText(questTitle);
}
/**
* Close popup by clicking elsewhere
*/
async closePopup(): Promise<void> {
await this.mapElement().click({ position: { x: 10, y: 10 } });
}
}