Implements website feature branch `410-public-docs-ia`. Target branch: `website-dev`. Validation: - `corepack pnpm --filter @tenantatlas/website build` - Playwright smoke coverage for public routes and docs interactions - Static claim scans for `apps/website/src`, `apps/website/public`, and `apps/website/dist` Follow-up integration path after merge: `website-dev` -> `dev`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #412
977 lines
28 KiB
TypeScript
977 lines
28 KiB
TypeScript
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,
|
|
},
|
|
'/platform/review-packs': {
|
|
title: /Review Packs \| Tenantial/i,
|
|
description:
|
|
/(?=.*Review Packs)(?=.*Evidence)(?=.*Accepted Risks)(?=.*Decision Summaries)(?=.*Service-Reviews)(?=.*Audit-Vorbereitung)/i,
|
|
},
|
|
'/evaluierung': {
|
|
title: /Evaluierung & Rollout \| Tenantial/i,
|
|
description:
|
|
/Microsoft-365-Governance sichtbarer|Reviews beschleunigt|fokussierten Pilot/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,
|
|
},
|
|
'/docs/': {
|
|
title: /Dokumentation|Tenantial Dokumentation/i,
|
|
description: /Evaluation|Betrieb|Governance|Provider|Evidence|Drift/i,
|
|
},
|
|
'/docs/getting-started/': {
|
|
title: /Erste Schritte|Getting Started/i,
|
|
description: /Evaluierung|Pilot|Ablauf|Microsoft 365/i,
|
|
},
|
|
'/docs/microsoft-365-provider/': {
|
|
title: /Microsoft 365 Provider/i,
|
|
description: /Microsoft 365|Provider|Grenzen|Zugriff/i,
|
|
},
|
|
'/docs/permissions-data-access/': {
|
|
title: /Berechtigungen|Datenzugriff/i,
|
|
description: /Berechtigungen|Datenzugriff|Rollen|Lesender Zugriff/i,
|
|
},
|
|
'/docs/data-processing-trust/': {
|
|
title: /Datenverarbeitung|Trust/i,
|
|
description: /Datenschutz|Trust|Dokumente|Grenzen/i,
|
|
},
|
|
'/docs/policy-evidence/': {
|
|
title: /Policy Evidence|Evidence/i,
|
|
description: /Evidence|Konfigurationsstand|Review|Nachweise/i,
|
|
},
|
|
'/docs/drift-detection/': {
|
|
title: /Drift Detection|Drift/i,
|
|
description: /Drift|Aenderungen|Review|Priorisierung/i,
|
|
},
|
|
'/docs/backups-versioning-recovery/': {
|
|
title: /Backups|Versionierung|Recovery/i,
|
|
description: /Backups|Versionierung|Recovery|Wiederherstellung/i,
|
|
},
|
|
'/docs/findings-exceptions-accepted-risk/': {
|
|
title: /Findings|Ausnahmen|Accepted Risk/i,
|
|
description: /Findings|Ausnahmen|Accepted Risks|Entscheidungen/i,
|
|
},
|
|
'/docs/review-packs-decisions/': {
|
|
title: /Review Packs|Entscheidungen/i,
|
|
description: /Review Packs|Entscheidungen|Evidence|Follow-up/i,
|
|
},
|
|
'/docs/evaluation-pilot/': {
|
|
title: /Evaluierung|Pilot/i,
|
|
description: /Evaluierung|Pilot|Readiness|Walkthrough/i,
|
|
},
|
|
'/docs/known-limitations/': {
|
|
title: /Bekannte Grenzen|Limitations/i,
|
|
description: /Grenzen|Fokus|keine Zusagen|heute nicht/i,
|
|
},
|
|
'/docs/faq/': {
|
|
title: /FAQ/i,
|
|
description: /FAQ|Fragen|Antworten|Einordnung/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/platform/review-packs': {
|
|
title: /Review Packs \| Tenantial/i,
|
|
description:
|
|
/(?=.*Review Packs)(?=.*evidence)(?=.*accepted risks)(?=.*decision summaries)(?=.*service reviews)(?=.*audit preparation)/i,
|
|
},
|
|
'/en/evaluation': {
|
|
title: /Evaluation & Rollout \| Tenantial/i,
|
|
description:
|
|
/Microsoft 365 governance clearer|speeds up reviews|focused pilot/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/docs/': {
|
|
title: /Documentation|Tenantial documentation/i,
|
|
description: /evaluation|operations|governance|provider|evidence|drift/i,
|
|
},
|
|
'/en/docs/getting-started/': {
|
|
title: /Getting Started/i,
|
|
description: /evaluation|pilot|workflow|Microsoft 365/i,
|
|
},
|
|
'/en/docs/microsoft-365-provider/': {
|
|
title: /Microsoft 365 Provider/i,
|
|
description: /Microsoft 365|provider|boundaries|access/i,
|
|
},
|
|
'/en/docs/permissions-data-access/': {
|
|
title: /Permissions|Data Access/i,
|
|
description: /permissions|data access|roles|read-oriented/i,
|
|
},
|
|
'/en/docs/data-processing-trust/': {
|
|
title: /Data Processing|Trust/i,
|
|
description: /privacy|trust|documents|boundaries/i,
|
|
},
|
|
'/en/docs/policy-evidence/': {
|
|
title: /Policy Evidence|Evidence/i,
|
|
description: /evidence|configuration state|review|proof points/i,
|
|
},
|
|
'/en/docs/drift-detection/': {
|
|
title: /Drift Detection|Drift/i,
|
|
description: /drift|changes|review|prioritization/i,
|
|
},
|
|
'/en/docs/backups-versioning-recovery/': {
|
|
title: /Backups|Versioning|Recovery/i,
|
|
description: /backups|versioning|recovery|restore context/i,
|
|
},
|
|
'/en/docs/findings-exceptions-accepted-risk/': {
|
|
title: /Findings|Exceptions|Accepted Risk/i,
|
|
description: /findings|exceptions|accepted risks|decisions/i,
|
|
},
|
|
'/en/docs/review-packs-decisions/': {
|
|
title: /Review Packs|Decisions/i,
|
|
description: /review packs|decisions|evidence|follow-up/i,
|
|
},
|
|
'/en/docs/evaluation-pilot/': {
|
|
title: /Evaluation|Pilot/i,
|
|
description: /evaluation|pilot|readiness|walkthrough/i,
|
|
},
|
|
'/en/docs/known-limitations/': {
|
|
title: /Known Limitations/i,
|
|
description: /limitations|focus|no promises|not today/i,
|
|
},
|
|
'/en/docs/faq/': {
|
|
title: /FAQ/i,
|
|
description: /FAQ|questions|answers|framing/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',
|
|
'/platform/review-packs',
|
|
'/en/use-cases/msp',
|
|
'/en/use-cases/mittelstand',
|
|
'/en/platform/review-packs',
|
|
] 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, linkName, href, footerHref } of [
|
|
{
|
|
route: '/',
|
|
linkName: 'Review-Pack-Story ansehen',
|
|
href: '/platform/review-packs',
|
|
footerHref: '/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/platform',
|
|
linkName: 'Review Packs verstehen',
|
|
href: '/platform/review-packs',
|
|
footerHref: '/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/use-cases/msp',
|
|
linkName: 'Review-Pack-Story für Kunden zeigen',
|
|
href: '/platform/review-packs',
|
|
footerHref: '/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/use-cases/mittelstand',
|
|
linkName: 'Review-Pack-Story für Reviews',
|
|
href: '/platform/review-packs',
|
|
footerHref: '/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/en/',
|
|
linkName: 'Explore review-pack story',
|
|
href: '/en/platform/review-packs',
|
|
footerHref: '/en/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/en/platform',
|
|
linkName: 'See review-pack story',
|
|
href: '/en/platform/review-packs',
|
|
footerHref: '/en/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/en/use-cases/msp',
|
|
linkName: 'Show the review-pack story',
|
|
href: '/en/platform/review-packs',
|
|
footerHref: '/en/platform/review-packs',
|
|
},
|
|
{
|
|
route: '/en/use-cases/mittelstand',
|
|
linkName: 'Review-pack story for reviews',
|
|
href: '/en/platform/review-packs',
|
|
footerHref: '/en/platform/review-packs',
|
|
},
|
|
] as const) {
|
|
test(`${route} exposes review-pack discovery through contextual and footer links`, async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(route);
|
|
|
|
await expect(
|
|
page.getByRole('link', { name: linkName, exact: true }).first()
|
|
).toHaveAttribute('href', href);
|
|
await expect(
|
|
page.getByRole('link', { name: 'Review Packs', exact: true }).last()
|
|
).toHaveAttribute('href', footerHref);
|
|
await expect(page.locator(`footer a[href="${footerHref}"]`)).toBeVisible();
|
|
await expectNoPlaceholderLinks(page);
|
|
});
|
|
}
|
|
|
|
for (const { route, linkName, href, footerName, footerHref } of [
|
|
{
|
|
route: '/',
|
|
linkName: 'Evaluierung & Pilot ansehen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/platform',
|
|
linkName: 'Evaluierung & Rollout ansehen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/platform/review-packs',
|
|
linkName: 'Evaluierungspfad ansehen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/trust',
|
|
linkName: 'Evaluierung & Procurement ansehen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/use-cases/msp',
|
|
linkName: 'Evaluierung & Pilot planen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/use-cases/mittelstand',
|
|
linkName: 'Evaluierung & Pilot planen',
|
|
href: '/evaluierung',
|
|
footerName: 'Evaluierung',
|
|
footerHref: '/evaluierung',
|
|
},
|
|
{
|
|
route: '/en/',
|
|
linkName: 'See evaluation and pilot',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
{
|
|
route: '/en/platform',
|
|
linkName: 'See evaluation and rollout',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
{
|
|
route: '/en/platform/review-packs',
|
|
linkName: 'See the evaluation path',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
{
|
|
route: '/en/trust',
|
|
linkName: 'See evaluation and procurement',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
{
|
|
route: '/en/use-cases/msp',
|
|
linkName: 'Plan evaluation and pilot',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
{
|
|
route: '/en/use-cases/mittelstand',
|
|
linkName: 'Plan evaluation and pilot',
|
|
href: '/en/evaluation',
|
|
footerName: 'Evaluation',
|
|
footerHref: '/en/evaluation',
|
|
},
|
|
] as const) {
|
|
test(`${route} exposes evaluation discovery through contextual and footer links`, async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(route);
|
|
|
|
await expect(
|
|
page.getByRole('link', { name: linkName, exact: true }).first()
|
|
).toHaveAttribute('href', href);
|
|
await expect(
|
|
page.getByRole('link', { name: footerName, exact: true }).last()
|
|
).toHaveAttribute('href', footerHref);
|
|
await expect(page.locator(`footer a[href="${footerHref}"]`)).toBeVisible();
|
|
await expectNoPlaceholderLinks(page);
|
|
});
|
|
}
|
|
|
|
for (const { route, locale } of [
|
|
{ route: '/evaluierung', locale: 'de' },
|
|
{ route: '/en/evaluation', locale: 'en' },
|
|
] as const) {
|
|
test(`${route} renders the evaluation path conservatively`, async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(route);
|
|
|
|
const headingPattern =
|
|
locale === 'de'
|
|
? /(Evaluierungspfad|evaluieren|pilotieren)/i
|
|
: /(evaluation path|Evaluate Tenantial|controlled demo)/i;
|
|
await expect(page.locator('main')).toContainText(headingPattern);
|
|
|
|
const securityPattern =
|
|
locale === 'de'
|
|
? /(Security & Procurement|Procurement|Checkliste)/i
|
|
: /(Security & procurement|procurement|checklist)/i;
|
|
await expect(page.locator('main')).toContainText(securityPattern);
|
|
|
|
const accessPattern =
|
|
locale === 'de'
|
|
? /(Microsoft-365-Zugriff|Bereitschafts|Berechtigung)/i
|
|
: /(Microsoft 365 access|readiness|permission)/i;
|
|
await expect(page.locator('main')).toContainText(accessPattern);
|
|
|
|
const faqPattern = locale === 'de' ? /FAQ|Fragen/i : /FAQ|questions/i;
|
|
await expect(page.locator('main')).toContainText(faqPattern);
|
|
|
|
const contactPath = locale === 'de' ? '/contact' : '/en/contact';
|
|
await expect(
|
|
page.locator(`main a[href="${contactPath}"]`).first()
|
|
).toBeVisible();
|
|
|
|
await expectNoForbiddenPublicClaims(page);
|
|
await expectNoProviderOrDataOverclaims(page);
|
|
await expectNoFakeTrustDownloads(page);
|
|
await expectNoPlaceholderLinks(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, docsHref } of [
|
|
{ route: '/', docsHref: '/docs/' },
|
|
{ route: '/en/', docsHref: '/en/docs/' },
|
|
] as const) {
|
|
test(`${route} exposes docs through global navigation and footer`, async ({
|
|
page,
|
|
isMobile,
|
|
}) => {
|
|
await page.goto(route);
|
|
|
|
if (isMobile) {
|
|
await page.getByLabel('Toggle navigation').click();
|
|
}
|
|
|
|
await expect(
|
|
page.locator('header').getByRole('link', { name: 'Docs', exact: true })
|
|
).toHaveAttribute('href', docsHref);
|
|
await expect(
|
|
page.locator('footer').getByRole('link', { name: 'Docs', exact: true })
|
|
).toHaveAttribute('href', docsHref);
|
|
await expect(page.locator(`footer a[href="${docsHref}"]`)).toBeVisible();
|
|
await expectNoPlaceholderLinks(page);
|
|
});
|
|
}
|
|
|
|
for (const { route, links } of [
|
|
{
|
|
route: '/',
|
|
links: [{ name: 'Dokumentation öffnen', href: '/docs/' }],
|
|
},
|
|
{
|
|
route: '/platform',
|
|
links: [{ name: 'Dokumentation öffnen', href: '/docs/' }],
|
|
},
|
|
{
|
|
route: '/trust',
|
|
links: [
|
|
{
|
|
name: 'Docs zu Datenschutz & Trust',
|
|
href: '/docs/data-processing-trust/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/evaluierung',
|
|
links: [
|
|
{
|
|
name: 'Docs zur Pilot-Vorbereitung',
|
|
href: '/docs/evaluation-pilot/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/platform/review-packs',
|
|
links: [
|
|
{ name: 'Docs zu Review Packs', href: '/docs/review-packs-decisions/' },
|
|
],
|
|
},
|
|
{
|
|
route: '/use-cases/msp',
|
|
links: [
|
|
{
|
|
name: 'Docs für Evaluierung & Einstieg',
|
|
href: '/docs/getting-started/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/use-cases/mittelstand',
|
|
links: [
|
|
{
|
|
name: 'Docs für Evaluierung & Einstieg',
|
|
href: '/docs/getting-started/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/en/',
|
|
links: [{ name: 'Open documentation', href: '/en/docs/' }],
|
|
},
|
|
{
|
|
route: '/en/platform',
|
|
links: [{ name: 'Open documentation', href: '/en/docs/' }],
|
|
},
|
|
{
|
|
route: '/en/trust',
|
|
links: [
|
|
{
|
|
name: 'Docs for privacy and trust',
|
|
href: '/en/docs/data-processing-trust/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/en/evaluation',
|
|
links: [
|
|
{
|
|
name: 'Docs for pilot readiness',
|
|
href: '/en/docs/evaluation-pilot/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/en/platform/review-packs',
|
|
links: [
|
|
{
|
|
name: 'Docs for review packs',
|
|
href: '/en/docs/review-packs-decisions/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/en/use-cases/msp',
|
|
links: [
|
|
{
|
|
name: 'Docs for onboarding and evaluation',
|
|
href: '/en/docs/getting-started/',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
route: '/en/use-cases/mittelstand',
|
|
links: [
|
|
{
|
|
name: 'Docs for onboarding and evaluation',
|
|
href: '/en/docs/getting-started/',
|
|
},
|
|
],
|
|
},
|
|
] as const) {
|
|
test(`${route} exposes contextual docs discovery`, async ({ page }) => {
|
|
await page.goto(route);
|
|
|
|
for (const link of links) {
|
|
await expect(
|
|
page.getByRole('link', { name: link.name, exact: true }).first()
|
|
).toHaveAttribute('href', link.href);
|
|
}
|
|
|
|
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}/?$`));
|
|
|
|
const targetMetadata =
|
|
routeMetadata[expected.target as keyof typeof routeMetadata];
|
|
|
|
if (targetMetadata) {
|
|
await expectMetadataForRoute(page, expected.target, targetMetadata);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|