first commit
This commit is contained in:
219
tests/e2e/core/pages/base.page.ts
Normal file
219
tests/e2e/core/pages/base.page.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Base page object with common methods shared across all pages
|
||||
*/
|
||||
export class BasePage {
|
||||
readonly page: Page
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the page to finish loading
|
||||
*/
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the toast notification element (first/most recent one)
|
||||
* Filters out toasts that are being removed to avoid matching stale toasts.
|
||||
* Optionally filters by toast type (success, error, loading, default).
|
||||
*/
|
||||
getToast(type?: 'success' | 'error' | 'loading' | 'default'): Locator {
|
||||
const selector = type
|
||||
? `[data-sonner-toast][data-type="${type}"]:not([data-removed="true"])`
|
||||
: '[data-sonner-toast]:not([data-removed="true"])'
|
||||
return this.page.locator(selector).first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a success toast to appear
|
||||
*/
|
||||
async waitForSuccessToast(message?: string): Promise<void> {
|
||||
const toast = this.getToast('success')
|
||||
await expect(toast).toBeVisible({ timeout: 10000 })
|
||||
if (message) {
|
||||
await expect(toast).toContainText(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an error toast to appear
|
||||
*/
|
||||
async waitForErrorToast(message?: string): Promise<void> {
|
||||
const toast = this.getToast('error')
|
||||
await expect(toast).toBeVisible({ timeout: 10000 })
|
||||
if (message) {
|
||||
await expect(toast).toContainText(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all toasts to disappear
|
||||
*/
|
||||
async waitForToastsToDisappear(timeout = 5000): Promise<void> {
|
||||
const toasts = this.page.locator('[data-sonner-toast]:not([data-removed="true"])')
|
||||
try {
|
||||
// Wait for all toasts to be detached from DOM
|
||||
await toasts.first().waitFor({ state: 'detached', timeout }).catch(() => {
|
||||
// If no toasts exist, that's fine
|
||||
})
|
||||
// Also check if count is 0
|
||||
const count = await toasts.count()
|
||||
if (count > 0) {
|
||||
// Wait for toasts to be hidden
|
||||
await expect(toasts.first()).not.toBeVisible({ timeout: 3000 }).catch(() => {})
|
||||
}
|
||||
} catch {
|
||||
// No toasts present, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a sheet/dialog to be fully visible (animation complete)
|
||||
*/
|
||||
async waitForSheetAnimation(): Promise<void> {
|
||||
// Wait for any sheet transition to complete by checking for stable state
|
||||
await this.page.waitForFunction(() => {
|
||||
const sheet = document.querySelector('[role="dialog"]')
|
||||
if (!sheet) return true
|
||||
const style = window.getComputedStyle(sheet)
|
||||
return style.opacity === '1' && style.transform === 'none'
|
||||
}, { timeout: 2000 }).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element state to change (useful for toggles)
|
||||
*/
|
||||
async waitForStateChange(locator: Locator, attribute: string, expectedValue: string, timeout = 5000): Promise<void> {
|
||||
await expect(locator).toHaveAttribute(attribute, expectedValue, { timeout })
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for URL to contain a specific parameter
|
||||
*/
|
||||
async waitForUrlParam(param: string, value: string, timeout = 5000): Promise<void> {
|
||||
await expect(this.page).toHaveURL(new RegExp(`${param}=${value}`), { timeout })
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for charts/data to load after page navigation
|
||||
*/
|
||||
async waitForChartsToLoad(): Promise<void> {
|
||||
// Wait for network to be idle (data fetching complete)
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
// Wait for any loading skeletons to disappear
|
||||
const skeletons = this.page.locator('[data-testid="skeleton"], .skeleton, [data-loading="true"]')
|
||||
if (await skeletons.count() > 0) {
|
||||
await skeletons.first().waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss all visible toasts by waiting for them to disappear
|
||||
*/
|
||||
async dismissToasts(): Promise<void> {
|
||||
// Just wait for toasts to auto-dismiss
|
||||
await this.waitForToastsToDisappear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Force dismiss all toasts by clicking away and waiting
|
||||
*/
|
||||
async forceCloseToasts(): Promise<void> {
|
||||
// Click somewhere neutral to potentially dismiss toasts
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 }, force: true }).catch(() => {})
|
||||
|
||||
// Wait for toasts to auto-dismiss (they typically auto-dismiss after 4-5 seconds)
|
||||
await this.waitForToastsToDisappear(8000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Dev Profiler overlay if it is visible.
|
||||
* Clicks the dismiss (X) button on the profiler panel. Silently continues if not present.
|
||||
*/
|
||||
async closeDevProfiler(): Promise<void> {
|
||||
const profilerHeader = this.page.locator('text=Dev Profiler')
|
||||
const isVisible = await profilerHeader.isVisible().catch(() => false)
|
||||
if (isVisible) {
|
||||
const dismissBtn = this.page.locator('button[title="Dismiss"]')
|
||||
if (await dismissBtn.isVisible().catch(() => false)) {
|
||||
await dismissBtn.click()
|
||||
await profilerHeader.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form field by label
|
||||
*/
|
||||
async fillByLabel(label: string, value: string): Promise<void> {
|
||||
await this.page.getByLabel(label).fill(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form field by placeholder
|
||||
*/
|
||||
async fillByPlaceholder(placeholder: string, value: string): Promise<void> {
|
||||
await this.page.getByPlaceholder(placeholder).fill(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a form field by test id
|
||||
*/
|
||||
async fillByTestId(testId: string, value: string): Promise<void> {
|
||||
await this.page.getByTestId(testId).fill(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a button by text
|
||||
*/
|
||||
async clickButton(text: string): Promise<void> {
|
||||
await this.page.getByRole('button', { name: text }).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a button by test id
|
||||
*/
|
||||
async clickByTestId(testId: string): Promise<void> {
|
||||
await this.page.getByTestId(testId).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an option from a dropdown by label
|
||||
*/
|
||||
async selectOption(label: string, value: string): Promise<void> {
|
||||
await this.page.getByLabel(label).selectOption(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element is visible
|
||||
*/
|
||||
async isVisible(selector: string): Promise<boolean> {
|
||||
return await this.page.locator(selector).isVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an element to be visible
|
||||
*/
|
||||
async waitForSelector(selector: string, timeout = 10000): Promise<void> {
|
||||
await this.page.waitForSelector(selector, { timeout })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text content of an element
|
||||
*/
|
||||
async getTextContent(selector: string): Promise<string | null> {
|
||||
return await this.page.locator(selector).textContent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot
|
||||
*/
|
||||
async screenshot(name: string): Promise<void> {
|
||||
await this.page.screenshot({ path: `./screenshots/${name}.png` })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user