import { expect, test } from '@playwright/test'; import { canonicalSiteUrl, docsRoutes, expectCoreCapabilitiesVisible, expectMetadataForRoute, expectNoForbiddenPublicClaims, expectNoFakeTrustDownloads, expectNoHorizontalOverflow, expectNoPlaceholderLinks, expectNoProviderOrDataOverclaims, expectPublicLinksAreIntentional, expectSitemapExcludesRoutes, expectSitemapIncludesRoutes, readSitemapUrls, redirectRouteExpectations, redirectRoutes, renderedRoutes, expectTrustHandoffsAreReal, expectTrustSurfaceVisible, } from './smoke-helpers'; import { readFile } from 'node:fs/promises'; import { globby } from 'globby'; const routeMetadata = { '/': { title: /Tenantial.*Microsoft 365 Policies unter Kontrolle bringen/i, description: /Policy-Drift früh erkennen|Konfigurationsstände versioniert sichern/i, }, '/use-cases/msp': { title: /MSP.*Tenantial|Tenantial.*MSP/i, description: /Microsoft 365 Governance|Reviews|Evidence|Backups|Accepted Risks/i, }, '/use-cases/mittelstand': { title: /Mittelstand|Enterprise IT|Tenantial/i, description: /Microsoft 365 Governance|Policy-Drift|Evidence|Backups|Recovery/i, }, '/platform': { title: /Plattform \| Tenantial/i, description: /Policy Governance|Microsoft 365/i, }, '/pricing': { title: /Preise \| Tenantial/i, description: /Policy-Governance-Evaluierung|Rollout-Planung/i, }, '/contact': { title: /Kontakt \| Tenantial/i, description: /Walkthrough|Provider-Grenzen|Policy-Governance/i, }, '/trust': { title: /Vertrauen, Datenschutz & Security \| Tenantial/i, description: /DACH-Evaluierungen|Dokumentenstatus|Provider-Berechtigungen/i, }, '/legal': { title: /Rechtliches \| Tenantial/i, description: /rechtliche Website-Informationen/i, }, '/privacy': { title: /Datenschutz \| Tenantial/i, description: /Website-Datenschutzhinweis/i, }, '/terms': { title: /Nutzungsbedingungen \| Tenantial/i, description: /Website-Nutzungsbedingungen/i, }, '/imprint': { title: /Impressum \| Tenantial/i, description: /Veröffentlichungsinformationen/i, }, '/welcome-to-docs/': { title: /Tenantial Docs/i, description: /Policy-Governance-Modell für Microsoft 365/i, }, '/guides/intro/': { title: /Einführung in Tenantial/i, description: /Policy-Governance-Modell für Microsoft 365/i, }, '/guides/getting-started/': { title: /Getting Started/i, description: /Tenant-Governance-Problem|Tenantial Walkthrough/i, }, '/guides/first-project-checklist/': { title: /Evaluierungscheckliste/i, description: /frühe Tenantial-Evaluierung|Checkliste/i, }, '/platform/evidence-review/': { title: /Evidence Review/i, description: /Policy-Governance-Modell|Evidence-Review-Workflows/i, }, '/en/': { title: /Tenantial.*Bring Microsoft 365 policies under control/i, description: /Detect Microsoft 365 policy drift early|versioned configuration backups/i, }, '/en/use-cases/msp': { title: /MSP.*Tenantial|Tenantial.*MSP/i, description: /Microsoft 365 governance|reviews|evidence|backups|accepted risks/i, }, '/en/use-cases/mittelstand': { title: /Enterprise IT|Mittelstand|Tenantial/i, description: /Microsoft 365 governance|policy drift|evidence|backups|recovery/i, }, '/en/platform': { title: /Platform \| Tenantial/i, description: /policy governance|Microsoft 365/i, }, '/en/pricing': { title: /Pricing \| Tenantial/i, description: /policy-governance evaluation|rollout planning/i, }, '/en/contact': { title: /Contact \| Tenantial/i, description: /walkthrough|provider boundaries|policy governance/i, }, '/en/trust': { title: /Trust, Privacy & Security \| Tenantial/i, description: /DACH evaluation|document status|provider permissions/i, }, '/en/legal': { title: /Legal \| Tenantial/i, description: /public website legal information/i, }, '/en/privacy': { title: /Privacy \| Tenantial/i, description: /public website privacy notice/i, }, '/en/terms': { title: /Terms \| Tenantial/i, description: /public website terms/i, }, '/en/imprint': { title: /Imprint \| Tenantial/i, description: /public website publication information/i, }, '/en/welcome-to-docs/': { title: /Tenantial Docs/i, description: /policy-governance model for Microsoft 365/i, }, '/en/guides/intro/': { title: /Introduction to Tenantial/i, description: /policy-governance model for Microsoft 365/i, }, '/en/guides/getting-started/': { title: /Getting Started/i, description: /Tenantial walkthrough|governance problem/i, }, '/en/guides/first-project-checklist/': { title: /Evaluation Checklist/i, description: /conservative checklist|early Tenantial evaluation/i, }, '/en/platform/evidence-review/': { title: /Evidence Review/i, description: /policy-governance model|evidence-review workflows/i, }, } as const; for (const route of renderedRoutes) { test(`renders intentional route ${route}`, async ({ page }) => { await page.goto(route); await expect(page.locator('body')).toContainText('Tenantial'); await expectNoForbiddenPublicClaims(page); await expectNoHorizontalOverflow(page); }); } test('homepage first viewport explains core Tenantial capabilities', async ({ page, }) => { await page.goto('/'); const heading = page.getByRole('heading', { name: /Microsoft 365 Policies unter Kontrolle bringen/i, }); await expect(heading).toBeVisible(); const box = await heading.boundingBox(); expect(box?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(420); await expect( page.getByRole('link', { name: /Demo buchen/i }).first() ).toHaveAttribute('href', '/contact'); await expect( page.getByRole('link', { name: /Plattform ansehen/i }).first() ).toHaveAttribute('href', '/platform'); await expect( page.getByRole('link', { name: /Plattform ansehen/i }).last() ).toHaveAttribute('href', '/platform'); await expectCoreCapabilitiesVisible(page); }); for (const { route, links } of [ { route: '/', links: [ { name: 'MSP-Use-Case ansehen', href: '/use-cases/msp' }, { name: 'Interne IT ansehen', href: '/use-cases/mittelstand' }, ], }, { route: '/en/', links: [ { name: 'Explore MSP use case', href: '/en/use-cases/msp' }, { name: 'Explore internal IT use case', href: '/en/use-cases/mittelstand', }, ], }, ] as const) { test(`${route} exposes localized use-case teaser links`, async ({ page }) => { await page.goto(route); const main = page.locator('main'); for (const link of links) { await expect( main.getByRole('link', { name: link.name, exact: true }) ).toHaveAttribute('href', link.href); } await expectNoPlaceholderLinks(page); }); } test('/platform explains the public product model without internal runtime terms', async ({ page, }) => { await page.goto('/platform'); await expect( page.getByRole('heading', { name: /Policy-Governance-Modell für Microsoft 365/i, }) ).toBeVisible(); await expectCoreCapabilitiesVisible(page); await expect(page.locator('body')).toContainText( /Aktueller Fokus: Microsoft 365/i ); await expect(page.locator('body')).toContainText( /weitere Policy-Domänen|Weitere Domänen/i ); await expect(page.locator('body')).toContainText( /authentifiziert keine Besucher, liest keinen Microsoft Tenant, führt keine Operationen aus und speichert keine Tenant-Exporte/i ); const text = await page.locator('body').innerText(); expect(text).not.toMatch(/\b(Laravel|Filament|Livewire)\b/i); expect(text).not.toMatch(/\bGraph runtime\b/i); }); for (const { route, links } of [ { route: '/platform', links: [ { name: 'MSP-Pfad ansehen', href: '/use-cases/msp' }, { name: 'Interne IT ansehen', href: '/use-cases/mittelstand' }, ], }, { route: '/en/platform', links: [ { name: 'See MSP path', href: '/en/use-cases/msp' }, { name: 'See internal IT path', href: '/en/use-cases/mittelstand' }, ], }, ] as const) { test(`${route} exposes compact buyer-path links`, async ({ page }) => { await page.goto(route); const main = page.locator('main'); for (const link of links) { await expect( main.getByRole('link', { name: link.name, exact: true }) ).toHaveAttribute('href', link.href); } await expectNoPlaceholderLinks(page); }); } for (const route of renderedRoutes) { test(`public links on ${route} resolve intentionally`, async ({ page, request, }) => { await page.goto(route); await expectNoPlaceholderLinks(page); await expectPublicLinksAreIntentional(page, request, route); }); } test('built HTML emits no placeholder href values', async () => { const htmlFiles = await globby('dist/**/*.html', { absolute: true, cwd: process.cwd(), }); expect(htmlFiles.length).toBeGreaterThan(0); for (const file of htmlFiles) { const html = await readFile(file, 'utf8'); expect(html, file).not.toMatch(/href=(["'])#\1/); expect(html, file).not.toMatch(/href=\{["']#["']\}/); } }); for (const route of [ '/trust', '/pricing', '/legal', '/privacy', '/terms', '/imprint', ...docsRoutes, ] as const) { test(`claim-sensitive route ${route} stays conservative`, async ({ page, }) => { await page.goto(route); await expectNoForbiddenPublicClaims(page); await expectNoHorizontalOverflow(page); }); } for (const route of [ '/use-cases/msp', '/use-cases/mittelstand', '/en/use-cases/msp', '/en/use-cases/mittelstand', ] as const) { test(`${route} keeps buyer-story claims conservative`, async ({ page }) => { await page.goto(route); await expectNoForbiddenPublicClaims(page); await expectNoProviderOrDataOverclaims(page); await expectNoHorizontalOverflow(page); }); } for (const { route, locale } of [ { route: '/trust', locale: 'de' }, { route: '/en/trust', locale: 'en' }, ] as const) { test(`${route} exposes the core trust posture conservatively`, async ({ page, }) => { await page.goto(route); await expectTrustSurfaceVisible(page, locale); await expectTrustHandoffsAreReal(page, locale); await expectNoProviderOrDataOverclaims(page); await expectNoHorizontalOverflow(page); }); } for (const { route, locale, contactPath } of [ { route: '/trust', locale: 'de', contactPath: '/contact' }, { route: '/en/trust', locale: 'en', contactPath: '/en/contact' }, ] as const) { test(`${route} keeps document readiness request-safe`, async ({ page }) => { await page.goto(route); await expectTrustHandoffsAreReal(page, locale); await expectNoFakeTrustDownloads(page); await expect( page.locator(`main a[href="${contactPath}"]`).first() ).toBeVisible(); await expect(page.locator('body')).toContainText(/DPA|AVV/); await expect(page.locator('body')).toContainText(/TOM/); await expect(page.locator('body')).toContainText( /Subprozessoren|Subprocessors/ ); await expect(page.locator('body')).toContainText( /Support-Zugriff|Support access/ ); await expect(page.locator('body')).toContainText( /Security- und Procurement-Fragen|Security and procurement questions/ ); }); } for (const route of ['/trust', '/en/trust'] as const) { test(`${route} explains data and permission boundaries without overclaims`, async ({ page, }) => { await page.goto(route); await expect(page.locator('body')).toContainText( /Data categories|Datenkategorien/ ); await expect(page.locator('body')).toContainText( /Provider permissions|Provider-Berechtigungen/ ); await expect(page.locator('body')).toContainText( /Read-oriented|Lesender Zugriff/ ); await expect(page.locator('body')).toContainText( /Write-oriented|Schreibender Zugriff/ ); await expect(page.locator('body')).toContainText( /Least privilege|Least Privilege/ ); await expect(page.locator('body')).toContainText( /Secrets and encryption|Secrets und Verschlüsselung/ ); await expectNoProviderOrDataOverclaims(page); }); } for (const { route, trustHref, label } of [ { route: '/', trustHref: '/trust', label: /Trust-Haltung ansehen|Vertrauen/ }, { route: '/en/', trustHref: '/en/trust', label: /Review trust posture|Trust/, }, ] as const) { test(`${route} exposes localized trust discoverability`, async ({ page }) => { await page.goto(route); await expect( page.getByRole('link', { name: label }).first() ).toHaveAttribute('href', trustHref); await expect(page.locator(`footer a[href="${trustHref}"]`)).toBeVisible(); await expectNoPlaceholderLinks(page); }); } for (const { route, navLinks } of [ { route: '/', navLinks: [ { name: 'MSPs', href: '/use-cases/msp' }, { name: 'Interne IT', href: '/use-cases/mittelstand' }, ], }, { route: '/en/', navLinks: [ { name: 'MSPs', href: '/en/use-cases/msp' }, { name: 'Internal IT', href: '/en/use-cases/mittelstand' }, ], }, ] as const) { test(`${route} exposes use-case links in navigation and footer`, async ({ page, isMobile, }) => { await page.goto(route); const header = page.locator('header'); const footer = page.locator('footer'); if (isMobile) { await page.getByLabel('Toggle navigation').click(); } for (const link of navLinks) { await expect( header.getByRole('link', { name: link.name, exact: true }) ).toHaveAttribute('href', link.href); await expect( footer.getByRole('link', { name: link.name, exact: true }) ).toHaveAttribute('href', link.href); } await expectNoPlaceholderLinks(page); }); } for (const [route, expected] of Object.entries(routeMetadata)) { test(`metadata is route-specific for ${route}`, async ({ page }) => { await page.goto(route); await expectMetadataForRoute(page, route, expected); }); } for (const route of redirectRoutes) { test(`redirects ${route} intentionally`, async ({ page, request }) => { const response = await request.get(route, { maxRedirects: 0 }); const expected = redirectRouteExpectations[route]; if (response.status() >= 300 && response.status() < 400) { expect(response.status()).toBe(expected.status); expect(response.headers().location).toBe(expected.target); } else { expect(response.status()).toBe(200); const html = await response.text(); expect(html).toContain('name="robots" content="noindex"'); expect(html).toContain( `rel="canonical" href="${canonicalSiteUrl}${expected.target}"` ); expect(html).toContain(`href="${expected.target}"`); } await page.goto(route); await expect(page).toHaveURL(new RegExp(`${expected.target}/?$`)); await expect( page.getByRole('heading', { name: /Policy-Governance-Modell für Microsoft 365|Policy governance model for Microsoft 365/, }) ).toBeVisible(); }); } test('robots and sitemap are available', async ({ page }) => { await page.goto('/robots.txt'); await expect(page.locator('body')).toContainText( `Sitemap: ${canonicalSiteUrl}/sitemap-index.xml` ); await page.goto('/sitemap-index.xml'); await expect(page.locator('body')).toContainText('sitemap-0.xml'); }); test('sitemap exposes canonical routes and excludes redirect aliases', async ({ page, }) => { const sitemapUrls = await readSitemapUrls(page); expectSitemapIncludesRoutes(sitemapUrls, renderedRoutes); expectSitemapExcludesRoutes(sitemapUrls, redirectRoutes); });