import { type Page, type Locator, expect } from '@playwright/test'; /** * Page Object Model for Navigation * Encapsulates header nav, footer nav, and mobile menu interactions */ export class NavigationPage { readonly page: Page; readonly header: Locator; readonly footer: Locator; readonly logo: Locator; readonly mobileMenuButton: Locator; readonly navLinks = { home: '/href="/"', about: '/about', services: '/services', portfolio: '/portfolio', blog: '/blog', contact: '/contact', }; constructor(page: Page) { this.page = page; this.header = page.locator('header'); this.footer = page.locator('footer'); this.logo = page.locator('header a').first(); this.mobileMenuButton = page.locator('button[aria-label*="menu"], button[aria-expanded], [data-menu-toggle]').first(); } async clickNavLink(path: string) { await this.page.locator(`header a[href="${path}"]`).first().click(); await expect(this.page).toHaveURL(path); } async openMobileMenu() { if (await this.mobileMenuButton.isVisible()) { await this.mobileMenuButton.click(); await this.page.waitForTimeout(300); // Wait for animation } } async closeMobileMenu() { if (await this.mobileMenuButton.isVisible()) { const isOpen = await this.mobileMenuButton.getAttribute('aria-expanded'); if (isOpen === 'true') { await this.mobileMenuButton.click(); await this.page.waitForTimeout(300); } } } async scrollToFooter() { await this.footer.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(200); } async clickFooterLink(path: string) { const link = this.footer.locator(`a[href="${path}"]`).first(); await link.click(); await expect(this.page).toHaveURL(path); } async verifyNoHorizontalOverflow(tolerance = 20) { const bodyWidth = await this.page.evaluate(() => document.body.scrollWidth); const viewportWidth = this.page.viewportSize()?.width ?? 1280; expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + tolerance); } }