import { expect, type APIRequestContext, type Page } from '@playwright/test'; export const renderedRoutes = [ '/', '/platform', '/pricing', '/contact', '/trust', '/legal', '/privacy', '/terms', '/imprint', '/welcome-to-docs/', '/guides/intro/', '/guides/getting-started/', '/guides/first-project-checklist/', '/platform/evidence-review/', '/en/', '/en/platform', '/en/pricing', '/en/contact', '/en/trust', '/en/legal', '/en/privacy', '/en/terms', '/en/imprint', '/en/welcome-to-docs/', '/en/guides/intro/', '/en/guides/getting-started/', '/en/guides/first-project-checklist/', '/en/platform/evidence-review/', ] as const; export const docsRoutes = [ '/welcome-to-docs/', '/guides/intro/', '/guides/getting-started/', '/guides/first-project-checklist/', '/platform/evidence-review/', '/en/welcome-to-docs/', '/en/guides/intro/', '/en/guides/getting-started/', '/en/guides/first-project-checklist/', '/en/platform/evidence-review/', ] as const; export const redirectRouteExpectations = { '/product': { status: 301, target: '/platform' }, '/products': { status: 301, target: '/platform' }, '/services': { status: 301, target: '/platform' }, '/blog': { status: 302, target: '/platform' }, '/insights': { status: 302, target: '/platform' }, '/en/product': { status: 301, target: '/en/platform' }, '/en/products': { status: 301, target: '/en/platform' }, '/en/services': { status: 301, target: '/en/platform' }, '/en/blog': { status: 302, target: '/en/platform' }, '/en/insights': { status: 302, target: '/en/platform' }, } as const; export const redirectRoutes = Object.keys(redirectRouteExpectations) as Array< keyof typeof redirectRouteExpectations >; export const staticRoutes = [ '/robots.txt', '/sitemap-index.xml', '/manifest.json', '/favicon.ico', ] as const; export const canonicalSiteUrl = 'https://tenantial.com'; const forbiddenPublicPatterns = [ { label: 'ScrewFast residue', pattern: /ScrewFast/i }, { label: 'construction residue', pattern: /\bconstruction\b/i }, { label: 'hardware residue', pattern: /\bhardware\b/i }, { label: 'template residue', pattern: /\btemplate\b/i }, { label: 'open-source residue', pattern: /open-source/i }, { label: 'TenantAtlas residue', pattern: /TenantAtlas/i }, { label: 'TenantPilot residue', pattern: /TenantPilot/i }, { label: 'TenantCTRL residue', pattern: /TenantCTRL/i }, { label: 'fake social proof', pattern: /trusted by/i }, { label: 'SOC 2 claim', pattern: /SOC\s*2/i }, { label: 'ISO 27001 claim', pattern: /ISO\s*27001/i }, { label: 'uptime claim', pattern: /99\.9%|guaranteed uptime/i }, { label: 'Microsoft endorsement', pattern: /Microsoft endorsed/i }, { label: 'Microsoft certification claim', pattern: /Microsoft certified/i }, { label: 'recovery guarantee', pattern: /guaranteed recovery/i }, { label: 'compliance guarantee', pattern: /guaranteed compliance/i }, { label: 'fake checkout CTA', pattern: /\b(buy now|checkout)\b/i }, { label: 'fake trial CTA', pattern: /\b(start free trial|create account)\b/i, }, { label: 'fake app access CTA', pattern: /\b(sign in|log in|login)\b/i }, ]; const metadataSelectors = [ 'title', 'meta[name="description"]', 'meta[property="og:title"]', 'meta[property="og:description"]', 'meta[property="og:url"]', 'meta[property="og:image"]', 'meta[property="twitter:url"]', 'meta[name="twitter:title"]', 'meta[name="twitter:description"]', 'meta[name="twitter:image"]', ].join(','); function normalizePath(path: string): string { if (path === '') { return '/'; } return path === '/' || path.includes('.') || path.endsWith('/') ? path : `${path}/`; } function normalizeRouteForSitemap(route: string): string { return route === '/' ? '/' : normalizePath(route); } function routeToCanonicalPath(route: string): string { return route === '' ? '/' : route; } export async function expectNoForbiddenPublicText(page: Page): Promise { const text = await page.locator('body').innerText(); for (const { label, pattern } of forbiddenPublicPatterns) { expect(text, label).not.toMatch(pattern); } } export async function expectNoForbiddenPublicClaims(page: Page): Promise { await expectNoForbiddenPublicText(page); const metadataText = await page .locator(metadataSelectors) .evaluateAll(nodes => nodes .map(node => node.textContent || node.getAttribute('content') || '') .join('\n') ); for (const { label, pattern } of forbiddenPublicPatterns) { expect(metadataText, `${label} in metadata`).not.toMatch(pattern); } } export async function expectNoHorizontalOverflow(page: Page): Promise { const metrics = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1); } export async function expectCoreCapabilitiesVisible(page: Page): Promise { const text = (await page.locator('body').innerText()).toLowerCase(); for (const terms of [ ['backup'], ['restore'], ['drift detection', 'drift'], ['findings'], ['evidence'], ['auditability'], ['exceptions'], ['reviews', 'review'], ]) { expect( terms.some(term => text.includes(term)), `missing capability term: ${terms.join(' or ')}` ).toBe(true); } } export async function expectNoPlaceholderLinks(page: Page): Promise { const placeholders = await page .locator('a[href="#"], a[href=""], area[href="#"], area[href=""]') .evaluateAll(nodes => nodes.map(node => ({ text: node.textContent?.trim() || '', href: node.getAttribute('href'), })) ); expect(placeholders).toEqual([]); } export async function expectPublicLinksAreIntentional( page: Page, request: APIRequestContext, sourceRoute: string ): Promise { const allowedRoutes = new Set( [ ...renderedRoutes, ...redirectRoutes, ...staticRoutes, '/sitemap-0.xml', ].map(route => normalizePath(route)) ); const hrefs = await page .locator('a[href], area[href]') .evaluateAll(nodes => nodes .map(node => node.getAttribute('href')?.trim()) .filter((href): href is string => Boolean(href)) ); for (const href of hrefs) { expect(href, `placeholder link on ${sourceRoute}`).not.toBe('#'); if ( href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:') ) { continue; } const url = new URL(href, canonicalSiteUrl); if (url.origin !== canonicalSiteUrl) { expect(['http:', 'https:']).toContain(url.protocol); continue; } const normalizedPath = normalizePath(url.pathname); expect( allowedRoutes.has(normalizedPath), `unexpected public link ${href} on ${sourceRoute}` ).toBe(true); if (url.hash && normalizePath(sourceRoute) === normalizedPath) { const targetId = url.hash.slice(1); await expect( page.locator(`#${targetId}`), `missing anchor target ${url.hash} on ${sourceRoute}` ).toHaveCount(1); } if (!url.hash || normalizePath(sourceRoute) !== normalizedPath) { const response = await request.get(url.pathname); expect( response.status(), `unexpected status for linked route ${href} on ${sourceRoute}` ).toBeLessThan(400); } } } export async function expectMetadataForRoute( page: Page, route: string, expected: { title: RegExp; description: RegExp } ): Promise { await expect(page).toHaveTitle(expected.title); await expect(page.locator('meta[name="description"]')).toHaveAttribute( 'content', expected.description ); const canonicalUrl = `${canonicalSiteUrl}${routeToCanonicalPath(route)}`; await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( 'href', canonicalUrl ); await expect(page.locator('meta[property="og:url"]').last()).toHaveAttribute( 'content', canonicalUrl ); await expect( page .locator('meta[property="twitter:url"], meta[name="twitter:url"]') .last() ).toHaveAttribute('content', canonicalUrl); await expect( page.locator('meta[property="og:image"]').last() ).toHaveAttribute('content', /\/_astro\/social\.[^/]+\.png$/); await expect( page.locator('meta[name="twitter:image"]').last() ).toHaveAttribute('content', /\/_astro\/social\.[^/]+\.png$/); } export function extractSitemapUrls(xml: string): string[] { return Array.from(xml.matchAll(/([^<]+)<\/loc>/g)).map(([, url]) => url); } export async function readSitemapUrls(page: Page): Promise { await page.goto('/sitemap-0.xml'); return extractSitemapUrls(await page.content()); } export function expectSitemapIncludesRoutes( urls: string[], routes: readonly string[] ): void { for (const route of routes) { const url = `${canonicalSiteUrl}${normalizeRouteForSitemap(route)}`; expect(urls, `sitemap missing ${url}`).toContain(url); } } export function expectSitemapExcludesRoutes( urls: string[], routes: readonly string[] ): void { for (const route of routes) { const url = `${canonicalSiteUrl}${normalizeRouteForSitemap(route)}`; expect(urls, `sitemap should exclude ${url}`).not.toContain(url); } }