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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { return this.markerPopup().isVisible({ timeout: 3000 }).catch(() => false); } /** * Get popup title */ async getPopupTitle(): Promise { return this.popupTitle().textContent().then(t => t || ''); } /** * Get popup description */ async getPopupDescription(): Promise { return this.popupDescription().textContent().then(t => t || ''); } /** * Get popup location */ async getPopupLocation(): Promise { return this.popupLocation().textContent().then(t => t || ''); } /** * Click popup action button * For sellers, this opens the bargaining interface */ async clickPopupActionButton(): Promise { 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 { await expect(this.popupTitle()).toContainText(questTitle); } /** * Close popup by clicking elsewhere */ async closePopup(): Promise { await this.mapElement().click({ position: { x: 10, y: 10 } }); } }