TenantAtlas/apps/website/tests/smoke/smoke-helpers.ts
ahmido c261b1c632 feat(website): tighten homepage messaging and trust flow (#398)
## Summary
- tighten homepage messaging, hero support copy, trust teaser flow, and CTA routing for the website public-content rollout
- align shared website copy, smoke expectations, and spec 404 artifacts with the latest messaging pass
- replace the previously closed PR for `404-public-content-messaging`

## Commits
- `44d27395` feat(website): tighten homepage messaging and trust flow
- `1ddbd28b` feat(website): refine public content messaging rollout

## Validation
- `git diff --check`

## Notes
- local Playwright MCP output remains untracked and was not included

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #398
2026-05-25 20:35:33 +00:00

338 lines
10 KiB
TypeScript

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: '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: 'autonomous remediation claim', pattern: /autonomous remediation/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);
}
}
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);
}
}