TenantAtlas/apps/website/tests/smoke/public-routes.spec.ts
ahmido 94cff6ca03 feat: add MSP and internal IT use-case pages (#402)
Implements website feature branch `407-msp-mittelstand-use-case-pages` into `website-dev`.

Summary:
- add German and English MSP and internal IT use-case landing pages
- expose localized buyer-path teasers from the homepage and platform page
- extend smoke coverage for the new routes, navigation links, metadata, and conservative claim checks
- include the aligned Spec Kit artifacts under `specs/407-msp-mittelstand-use-case-pages`

Validation:
- `cd apps/website && corepack pnpm build`
- `cd apps/website && corepack pnpm test -- tests/smoke/public-routes.spec.ts tests/smoke/interaction.spec.ts`

Follow-up integration path after merge:

`website-dev` -> `dev`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #402
2026-05-27 22:02:42 +00:00

535 lines
16 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,
},
'/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);
});