TenantAtlas/apps/website/tests/smoke/smoke-helpers.ts
ahmido af5fa30341 410: add public docs information architecture (#412)
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
2026-05-31 21:11:07 +00:00

605 lines
18 KiB
TypeScript

import { expect, type APIRequestContext, type Page } from '@playwright/test';
export const renderedRoutes = [
'/',
'/use-cases/msp',
'/use-cases/mittelstand',
'/platform',
'/platform/review-packs',
'/evaluierung',
'/pricing',
'/contact',
'/trust',
'/legal',
'/privacy',
'/terms',
'/imprint',
'/en/',
'/en/use-cases/msp',
'/en/use-cases/mittelstand',
'/en/platform',
'/en/platform/review-packs',
'/en/evaluation',
'/en/pricing',
'/en/contact',
'/en/trust',
'/en/legal',
'/en/privacy',
'/en/terms',
'/en/imprint',
'/docs/',
'/docs/getting-started/',
'/docs/microsoft-365-provider/',
'/docs/permissions-data-access/',
'/docs/data-processing-trust/',
'/docs/policy-evidence/',
'/docs/drift-detection/',
'/docs/backups-versioning-recovery/',
'/docs/findings-exceptions-accepted-risk/',
'/docs/review-packs-decisions/',
'/docs/evaluation-pilot/',
'/docs/known-limitations/',
'/docs/faq/',
'/en/docs/',
'/en/docs/getting-started/',
'/en/docs/microsoft-365-provider/',
'/en/docs/permissions-data-access/',
'/en/docs/data-processing-trust/',
'/en/docs/policy-evidence/',
'/en/docs/drift-detection/',
'/en/docs/backups-versioning-recovery/',
'/en/docs/findings-exceptions-accepted-risk/',
'/en/docs/review-packs-decisions/',
'/en/docs/evaluation-pilot/',
'/en/docs/known-limitations/',
'/en/docs/faq/',
] as const;
export const docsRoutes = [
'/docs/',
'/docs/getting-started/',
'/docs/microsoft-365-provider/',
'/docs/permissions-data-access/',
'/docs/data-processing-trust/',
'/docs/policy-evidence/',
'/docs/drift-detection/',
'/docs/backups-versioning-recovery/',
'/docs/findings-exceptions-accepted-risk/',
'/docs/review-packs-decisions/',
'/docs/evaluation-pilot/',
'/docs/known-limitations/',
'/docs/faq/',
'/en/docs/',
'/en/docs/getting-started/',
'/en/docs/microsoft-365-provider/',
'/en/docs/permissions-data-access/',
'/en/docs/data-processing-trust/',
'/en/docs/policy-evidence/',
'/en/docs/drift-detection/',
'/en/docs/backups-versioning-recovery/',
'/en/docs/findings-exceptions-accepted-risk/',
'/en/docs/review-packs-decisions/',
'/en/docs/evaluation-pilot/',
'/en/docs/known-limitations/',
'/en/docs/faq/',
] 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' },
'/welcome-to-docs/': { status: 301, target: '/docs/' },
'/guides/intro/': { status: 301, target: '/docs/getting-started/' },
'/guides/getting-started/': {
status: 301,
target: '/docs/getting-started/',
},
'/guides/first-project-checklist/': {
status: 301,
target: '/docs/evaluation-pilot/',
},
'/platform/evidence-review/': {
status: 301,
target: '/docs/policy-evidence/',
},
'/en/welcome-to-docs/': { status: 301, target: '/en/docs/' },
'/en/guides/intro/': {
status: 301,
target: '/en/docs/getting-started/',
},
'/en/guides/getting-started/': {
status: 301,
target: '/en/docs/getting-started/',
},
'/en/guides/first-project-checklist/': {
status: 301,
target: '/en/docs/evaluation-pilot/',
},
'/en/platform/evidence-review/': {
status: 301,
target: '/en/docs/policy-evidence/',
},
} 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: 'Intune-only tool claim', pattern: /Intune Management Tool/i },
{ label: 'backup-tool claim', pattern: /Intune backup tool/i },
{ label: 'DSGVO compliance claim', pattern: /DSGVO compliant/i },
{ label: 'GDPR compliance claim', pattern: /GDPR compliant/i },
{ label: 'ISO certified claim', pattern: /ISO certified/i },
{ label: 'Google supported claim', pattern: /Google supported/i },
{ label: 'AWS supported claim', pattern: /AWS supported/i },
{ label: 'automatic restore claim', pattern: /automatic restore/i },
{ label: 'one-click restore claim', pattern: /one[- ]click restore/i },
{ label: 'immutable backup claim', pattern: /immutable backups?/i },
{
label: 'real-time drift detection claim',
pattern: /real[- ]time drift(?: detection)?/i,
},
{ label: 'court-proof evidence claim', pattern: /court[- ]proof evidence/i },
{ label: 'autonomous remediation claim', pattern: /autonomous remediation/i },
{
label: 'customer-safe productization residue',
pattern: /customer-safe consumption productization/i,
},
{ label: 'route-owned residue', pattern: /route-owned/i },
{ label: 'artifact taxonomy residue', pattern: /artifact taxonomy/i },
{ label: 'source family residue', pattern: /source family/i },
{ label: 'capability registry residue', pattern: /capability registry/i },
{ label: 'repo-real foundation residue', pattern: /repo-real foundation/i },
{
label: 'gapless evidence claim',
pattern: /lueckenlose eviden(?:ce|z)|lückenlose eviden(?:ce|z)/i,
},
{
label: 'court-proof evidence claim in German',
pattern: /gerichtsfeste Nachweise/i,
},
{ label: 'immutable evidence claim', pattern: /immutable evidence/i },
{
label: 'immutable review packs claim',
pattern: /immutable review packs/i,
},
{ label: 'complete audit trail claim', pattern: /complete audit trail/i },
{
label: 'guaranteed audit success claim',
pattern: /guarantees audit success/i,
},
{ label: 'magic compliance claim', pattern: /macht Sie compliant/i },
{ label: 'DSGVO conform claim', pattern: /DSGVO-konform/i },
{ label: 'ISO certified claim in German', pattern: /ISO-zertifiziert/i },
{ label: 'neutral SaaS residue', pattern: /neutral SaaS visual/i },
{ label: 'lorem ipsum residue', pattern: /lorem ipsum/i },
{ label: 'fake checkout CTA', pattern: /\b(buy now|checkout)\b/i },
{ label: 'fake newsletter CTA', pattern: /\b(subscribe|newsletter)\b/i },
{
label: 'fake subscription CTA',
pattern: /\b(pay now|start subscription|activate subscription)\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<void> {
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<void> {
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<void> {
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<void> {
const text = (await page.locator('body').innerText()).toLowerCase();
for (const terms of [
['policy governance', 'policy-governance', 'governance'],
['microsoft 365'],
['evidence'],
['drift detection', 'policy-drift', 'drift'],
['reviews', 'review'],
['audit trail', 'auditability'],
['controlled recovery', 'recovery', 'restore'],
['provider', 'policy-domänen', 'policy domains', 'further policy domains'],
]) {
expect(
terms.some(term => text.includes(term)),
`missing capability term: ${terms.join(' or ')}`
).toBe(true);
}
}
const trustSurfaceExpectations = {
de: {
heading: /Prüfbare Grenzen für DACH-Evaluierungen/i,
statuses: [
'Dokumentiert',
'Auf Anfrage',
'In Vorbereitung',
'Geplant',
'Nicht beansprucht',
'Nicht anwendbar',
],
coreTerms: [
'Hosting- und Betriebsregion',
'Datenschutz- und DSGVO-Haltung',
'Auditierbarkeit und Review-Kontext',
'Retention, Export und Löschung',
'Support-Zugriff',
'Dokumenten- und Disclosure-Readiness',
'Datenkategorien und Grenzen',
'Provider-Berechtigungen und Zugriffsklassen',
'Least Privilege und RBAC',
'Secrets und Verschlüsselung',
'Sicherer Handoff für Trust-Fragen',
],
documentTerms: [
'AVV / DPA',
'TOM / technische und organisatorische Maßnahmen',
'Subprozessoren',
'Security- und Procurement-Fragen',
],
dataTerms: [
'Account- und Workspace-Metadaten',
'Managed-Environment- und Provider-Metadaten',
'Policy- und Konfigurations-Evidence',
'Findings, Exceptions und Review-Artefakte',
'Operation- und Audit-Metadaten',
'Support- und Diagnosekontext',
],
contactHref: '/contact',
},
en: {
heading: /Reviewable boundaries for DACH evaluation/i,
statuses: [
'Documented',
'On request',
'In preparation',
'Planned',
'Not claimed',
'Not applicable',
],
coreTerms: [
'Hosting and operating region',
'Privacy and GDPR posture',
'Auditability and review context',
'Retention, export, and deletion',
'Support access',
'Document and disclosure readiness',
'Data categories and boundaries',
'Provider permissions and access classes',
'Least privilege and RBAC',
'Secrets and encryption',
'Safe handoff for trust questions',
],
documentTerms: [
'DPA',
'TOM / technical and organizational measures',
'Subprocessors',
'Security and procurement questions',
],
dataTerms: [
'Account and workspace metadata',
'Managed environment and provider metadata',
'Policy and configuration evidence',
'Findings, exceptions, and review artifacts',
'Operation and audit metadata',
'Support and diagnostic context',
],
contactHref: '/en/contact',
},
} as const;
export async function expectTrustSurfaceVisible(
page: Page,
locale: keyof typeof trustSurfaceExpectations
): Promise<void> {
const expectations = trustSurfaceExpectations[locale];
const text = await page.locator('body').innerText();
await expect(
page.getByRole('heading', { name: expectations.heading })
).toBeVisible();
for (const term of [
...expectations.statuses,
...expectations.coreTerms,
...expectations.documentTerms,
...expectations.dataTerms,
]) {
expect(text, `missing trust term: ${term}`).toContain(term);
}
}
export async function expectTrustHandoffsAreReal(
page: Page,
locale: keyof typeof trustSurfaceExpectations
): Promise<void> {
const expectations = trustSurfaceExpectations[locale];
const handoffLinks = page.locator(`a[href="${expectations.contactHref}"]`);
expect(await handoffLinks.count()).toBeGreaterThan(0);
await expectNoPlaceholderLinks(page);
}
export async function expectNoFakeTrustDownloads(page: Page): Promise<void> {
const downloadLikeLinks = await page.locator('a[href]').evaluateAll(nodes =>
nodes
.map(node => ({
text: node.textContent?.trim() || '',
href: node.getAttribute('href') || '',
}))
.filter(
link =>
/\.(pdf|docx?|xlsx?|zip)([?#].*)?$/i.test(link.href) ||
/\b(download|herunterladen)\b/i.test(link.text)
)
);
expect(downloadLikeLinks).toEqual([]);
}
export async function expectNoProviderOrDataOverclaims(
page: Page
): Promise<void> {
const text = await page.locator('body').innerText();
for (const pattern of [
/Google supported/i,
/AWS supported/i,
/automatic remediation/i,
/automatic restore/i,
/real[- ]time drift(?: detection)?/i,
/immutable evidence/i,
/immutable review packs/i,
/complete audit trail/i,
/guarantees audit success/i,
/macht Sie compliant/i,
/DSGVO-konform/i,
/ISO-zertifiziert/i,
/court[- ]proof evidence/i,
/gerichtsfeste Nachweise/i,
/no customer data stored/i,
/no personal data/i,
/keine Kundendaten/i,
/keine personenbezogenen Daten/i,
]) {
expect(text).not.toMatch(pattern);
}
}
export async function expectNoPlaceholderLinks(page: Page): Promise<void> {
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<void> {
const allowedRoutes = new Set<string>(
[
...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<void> {
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>([^<]+)<\/loc>/g)).map(([, url]) => url);
}
export async function readSitemapUrls(page: Page): Promise<string[]> {
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);
}
}