282 lines
8.1 KiB
TypeScript
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 } });
|
|
}
|
|
}
|