TenantAtlas/apps/website/tests/smoke/smoke-helpers.ts

322 lines
9.3 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: 'fake checkout CTA', pattern: /\b(buy now|checkout)\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 [
['backup'],
['restore'],
['drift detection', 'drift'],
['findings'],
['evidence'],
['auditability'],
['exceptions'],
['reviews', 'review'],
]) {
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);
}
}