import { expect, type Page } from '@playwright/test'; export const coreRoutePaths = ['/', '/product', '/trust', '/changelog', '/contact', '/privacy', '/imprint'] as const; export const secondaryRoutePaths = ['/legal', '/terms', '/solutions', '/integrations'] as const; export const primaryNavigationLabels = ['Product', 'Trust', 'Changelog', 'Contact'] as const; export const hiddenPrimaryNavigationLabels = [ 'Solutions', 'Integrations', 'Security & Trust', 'Resources', 'Articles', ] as const; export const footerLabels = ['Product', 'Changelog', 'Trust', 'Privacy', 'Imprint', 'Terms', 'Contact'] as const; export const hiddenFooterLabels = ['Resources', 'Articles', 'Security & Trust', 'Contact / Demo'] as const; export async function visitPage(page: Page, path: string): Promise { await page.goto(path); await expect(page).toHaveURL(new RegExp(path === '/' ? '/?$' : `${path}$`)); } export async function expectCompatibilityRedirect(page: Page, legacyPath: string, canonicalPath: string): Promise { await page.goto(legacyPath); await page.waitForURL(new RegExp(canonicalPath === '/' ? '/?$' : `${canonicalPath}$`)); await expect(page).toHaveURL(new RegExp(canonicalPath === '/' ? '/?$' : `${canonicalPath}$`)); } export async function expectShell(page: Page, heading: string | RegExp): Promise { await expect(page.getByRole('banner')).toBeVisible(); await expect(page.getByRole('main')).toBeVisible(); await expect(page.getByRole('contentinfo')).toBeVisible(); await expect(page.getByRole('heading', { level: 1, name: heading })).toBeVisible(); } export async function expectPageFamily(page: Page, family: 'content' | 'landing' | 'trust'): Promise { await expect(page.locator(`[data-page-family="${family}"]`).first()).toBeVisible(); } export async function expectPrimaryNavigation(page: Page): Promise { const header = page.getByRole('banner'); for (const label of primaryNavigationLabels) { const link = header.getByRole('link', { name: label, exact: true }).first(); await expect(link).toBeVisible(); await expect(link).toHaveAttribute('data-nav-link'); } for (const label of hiddenPrimaryNavigationLabels) { await expect(header.getByRole('link', { name: label, exact: true })).toHaveCount(0); } } export async function expectFooterLinks(page: Page): Promise { for (const label of footerLabels) { await expect(page.getByRole('contentinfo').getByRole('link', { name: label, exact: true })).toBeVisible(); } for (const label of hiddenFooterLabels) { await expect(page.getByRole('contentinfo').getByRole('link', { name: label, exact: true })).toHaveCount(0); } } export async function openMobileNavigation(page: Page): Promise { const menuTrigger = page.getByLabel('Open navigation menu'); if (await menuTrigger.isVisible()) { await menuTrigger.click(); } } export async function expectDisclosureLayer(page: Page, layer: '1' | '2' | '3'): Promise { await expect(page.locator(`[data-disclosure-layer="${layer}"]`).first()).toBeVisible(); } export async function expectCtaHierarchy( page: Page, primaryLabel: string | RegExp, secondaryLabel: string | RegExp, ): Promise { const main = page.getByRole('main'); await expect(main.locator('[data-cta-weight="primary"]').filter({ hasText: primaryLabel }).first()).toBeVisible(); await expect( main.locator('[data-cta-weight="secondary"]').filter({ hasText: secondaryLabel }).first(), ).toBeVisible(); } export async function expectHomepageHeroStructure(page: Page): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); await expect(hero).toBeVisible(); await expect(hero.locator('[data-hero-text-core]').first()).toBeVisible(); await expect(hero.locator('[data-hero-eyebrow]').first()).toBeVisible(); await expect(hero.locator('[data-hero-heading]').getByRole('heading', { level: 1 })).toBeVisible(); await expect(hero.locator('[data-hero-supporting-copy]').first()).toBeVisible(); await expect(hero.locator('[data-hero-cta-pair]').first()).toBeVisible(); await expect(hero.locator('[data-cta-slot="primary"]')).toHaveCount(1); await expect(hero.locator('[data-cta-slot="secondary"]')).toHaveCount(1); await expect(hero.locator('[data-hero-visual]').first()).toBeVisible(); } export async function expectHomepageHeroCtaPair( page: Page, primaryLabel: string | RegExp, secondaryLabel: string | RegExp, ): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); await expect(hero.locator('[data-cta-weight="primary"]').filter({ hasText: primaryLabel }).first()).toBeVisible(); await expect( hero.locator('[data-cta-weight="secondary"]').filter({ hasText: secondaryLabel }).first(), ).toBeVisible(); } export async function expectHomepageHeroPrimaryAnchor( page: Page, anchor: 'headline' | 'product-visual' | 'composition', ): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); await expect(hero.locator(`[data-hero-primary-anchor="${anchor}"]`).first()).toBeVisible(); await expect(hero.locator('[data-hero-primary-anchor]')).toHaveCount(1); } export async function expectHomepageHeroSupportingCopySubordination(page: Page): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const heading = hero.locator('[data-hero-heading]').getByRole('heading', { level: 1 }).first(); const supportingCopy = hero.locator('[data-hero-supporting-copy] p').first(); await expect(hero.locator('[data-hero-supporting-copy]').first()).toHaveAttribute('data-hero-copy-role', 'supporting'); const [headingFontSize, supportingCopyFontSize] = await Promise.all([ heading.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).fontSize)), supportingCopy.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).fontSize)), ]); expect(headingFontSize, 'Hero heading should remain larger than supporting copy').toBeGreaterThan( supportingCopyFontSize, ); } export async function expectHomepageHeroAnchorCtaAlignment(page: Page): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const anchorGroup = hero.locator('[data-hero-anchor-group]').first(); await expect(anchorGroup).toBeVisible(); await expect(anchorGroup.locator('[data-hero-heading]').first()).toBeVisible(); await expect(anchorGroup.locator('[data-hero-cta-pair]').first()).toBeVisible(); } export async function expectHomepageHeroOrder( page: Page, segments: Array<'eyebrow' | 'headline' | 'supporting-copy' | 'cta-pair' | 'product-near-visual' | 'trust-subclaims'>, ): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const actual = await hero.locator('[data-hero-segment]').evaluateAll((elements) => elements .map((element) => element.getAttribute('data-hero-segment')) .filter(Boolean), ); for (let i = 0; i < segments.length; i++) { expect(actual.indexOf(segments[i]), `Hero segment "${segments[i]}" should exist`).toBeGreaterThanOrEqual(0); if (i > 0) { expect( actual.indexOf(segments[i]), `Hero segment "${segments[i]}" should appear after "${segments[i - 1]}"`, ).toBeGreaterThan(actual.indexOf(segments[i - 1])); } } } export async function expectHomepageHeroTrustSignals(page: Page): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const trustSignals = hero.locator('[data-hero-trust-signals] li'); const count = await trustSignals.count(); await expect(hero.locator('[data-hero-trust-signals]').first()).toBeVisible(); expect(count).toBeGreaterThan(0); expect(count).toBeLessThanOrEqual(3); } export async function expectHomepageHeroVisualSemantics( page: Page, terms: Array, ): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const visual = hero.locator('[data-hero-visual]').first(); await expect(visual).toBeVisible(); await expect(visual).toHaveAttribute('data-hero-visual-style', 'governance-surface'); for (const term of terms) { await expect(visual).toContainText(term); } } export async function expectHomepageHeroSplitLayout(page: Page): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); const textPanel = hero.locator('[data-hero-panel="text"]').first(); const visualPanel = hero.locator('[data-hero-panel="dashboard"]').first(); await expect(textPanel).toBeVisible(); await expect(visualPanel).toBeVisible(); const [textBox, visualBox] = await Promise.all([textPanel.boundingBox(), visualPanel.boundingBox()]); expect(textBox, 'Hero text panel should have a bounding box').not.toBeNull(); expect(visualBox, 'Hero visual panel should have a bounding box').not.toBeNull(); if (!textBox || !visualBox) { return; } expect( textBox.x + textBox.width, 'Desktop hero text should end before the visual surface begins so both read as one split composition', ).toBeLessThanOrEqual(visualBox.x + 48); expect( Math.abs(textBox.y - visualBox.y), 'Desktop hero text and visual should share a horizontal composition instead of stacking far apart', ).toBeLessThan(140); } async function expectLocatorInInitialViewport(page: Page, selector: string, label: string): Promise { const locator = page.locator(selector).first(); const box = await locator.boundingBox(); const viewport = page.viewportSize(); await expect(locator).toBeVisible(); expect(box, `${label} should have a bounding box`).not.toBeNull(); expect(viewport, 'Viewport should be available').not.toBeNull(); if (!box || !viewport) { return; } expect(box.y, `${label} should start within the initial viewport`).toBeLessThan(viewport.height); expect(box.y + Math.min(box.height, 32), `${label} should remain on screen at first paint`).toBeGreaterThan(0); } export async function expectHomepageHeroVisibleOnMobile(page: Page): Promise { await expectLocatorInInitialViewport( page, '[data-homepage-hero="true"] [data-hero-primary-anchor]', 'Hero primary anchor', ); await expectLocatorInInitialViewport(page, '[data-homepage-hero="true"] [data-hero-cta-pair]', 'Hero CTA pair'); await expect(page.locator('[data-homepage-hero="true"] [data-hero-visual]').first()).toBeVisible(); } export async function expectHomepageHeroRouteTargets(page: Page, routes: string[]): Promise { const hero = page.locator('[data-homepage-hero="true"]').first(); for (const route of routes) { await expect(hero.locator(`a[href="${route}"]`).first(), `Route "${route}" should be reachable from hero`).toBeVisible(); } } export async function expectNavigationVsCtaDifferentiation(page: Page): Promise { const header = page.getByRole('banner'); await expect(header.locator('[data-nav-link]').first()).toBeVisible(); await expect(header.locator('[data-cta-weight="secondary"]').first()).toBeVisible(); } export async function expectHomepageSectionOrder(page: Page, sections: string[]): Promise { const main = page.getByRole('main'); const sectionElements = main.locator('[data-section]'); const count = await sectionElements.count(); const actual: string[] = []; for (let i = 0; i < count; i++) { const name = await sectionElements.nth(i).getAttribute('data-section'); if (name) { actual.push(name); } } for (let i = 0; i < sections.length; i++) { expect(actual.indexOf(sections[i]), `Section "${sections[i]}" should appear in order`).toBeGreaterThanOrEqual(0); if (i > 0) { expect( actual.indexOf(sections[i]), `Section "${sections[i]}" should appear after "${sections[i - 1]}"`, ).toBeGreaterThan(actual.indexOf(sections[i - 1])); } } } export async function expectProductNearVisual(page: Page, alt?: string | RegExp): Promise { const main = page.getByRole('main'); const visual = main.locator('[data-hero-visual] img, [data-hero-visual]').first(); await expect(visual).toBeVisible(); if (alt) { await expect(main.getByRole('img', { name: alt }).first()).toBeVisible(); } } export async function expectMobileReadability(page: Page): Promise { const main = page.getByRole('main'); await expect(main).toBeVisible(); await expect(page.getByRole('banner')).toBeVisible(); await expect(page.getByRole('contentinfo')).toBeVisible(); } export async function expectOnwardRouteReachable(page: Page, routes: string[]): Promise { const main = page.getByRole('main'); for (const route of routes) { await expect( main.locator(`a[href="${route}"]`).first(), `Route "${route}" should be reachable from main content`, ).toBeVisible(); } }