The existing smoke test only checked TCP port connectivity, which cannot
detect a service that accepts connections but returns errors or serves
the wrong content (exactly how the broken Kiwix slipped through).
Add two new validation layers:
1. validate-http.sh — curls every service's health/UI endpoint and
asserts both the HTTP status code AND meaningful body content (JSON
health fields like "database":"ok", XML markers, page strings). Also
verifies Kiwix has a ZIM actually loaded via its OPDS catalog feed.
2. run-browser-tests.sh — drives a real headless Chromium via the
official Playwright Docker image (no host Node install needed) to
confirm pages render correctly after JavaScript execution and SPA
hydration. Updated playwright-services.spec.ts with correct content
tokens and a waitForFunction step that reliably handles SPA timing.
Both are wired into demo-test.sh (new `http` and `browser` subcommands)
and the full test suite. AGENTS.md and demo/AGENTS.md updated with the
new commands and protocols.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
145 lines
3.3 KiB
TypeScript
145 lines
3.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
const services = [
|
|
{
|
|
name: 'Homepage',
|
|
url: 'http://localhost:4000',
|
|
contentCheck: 'tsys developer support stack',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'Pi-hole',
|
|
url: 'http://localhost:4006/admin',
|
|
contentCheck: 'pihole',
|
|
},
|
|
{
|
|
name: 'Dockhand',
|
|
url: 'http://localhost:4007',
|
|
contentCheck: 'sveltekit',
|
|
},
|
|
{
|
|
name: 'InfluxDB',
|
|
url: 'http://localhost:4008',
|
|
contentCheck: 'influxdb',
|
|
},
|
|
{
|
|
name: 'Grafana',
|
|
url: 'http://localhost:4009',
|
|
contentCheck: 'grafana',
|
|
},
|
|
{
|
|
name: 'Draw.io',
|
|
url: 'http://localhost:4010',
|
|
contentCheck: 'diagram',
|
|
},
|
|
{
|
|
name: 'Kroki',
|
|
url: 'http://localhost:4011/health',
|
|
contentCheck: 'kroki',
|
|
},
|
|
{
|
|
name: 'Atomic Tracker',
|
|
url: 'http://localhost:4012',
|
|
contentCheck: 'atomic',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'ArchiveBox',
|
|
url: 'http://localhost:4013',
|
|
contentCheck: 'archive',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'Tube Archivist',
|
|
url: 'http://localhost:4014',
|
|
contentCheck: 'tube archivist',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'Wakapi',
|
|
url: 'http://localhost:4015',
|
|
contentCheck: 'wakapi',
|
|
},
|
|
{
|
|
name: 'MailHog',
|
|
url: 'http://localhost:4017',
|
|
contentCheck: 'mailhog',
|
|
},
|
|
{
|
|
name: 'Atuin',
|
|
url: 'http://localhost:4018',
|
|
contentCheck: 'version',
|
|
},
|
|
{
|
|
name: 'Reactive Resume',
|
|
url: 'http://localhost:4016',
|
|
contentCheck: 'reactive',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'Metrics',
|
|
url: 'http://localhost:4021',
|
|
contentCheck: 'metrics',
|
|
},
|
|
{
|
|
name: 'Kiwix',
|
|
url: 'http://localhost:4022',
|
|
contentCheck: 'kiwix',
|
|
},
|
|
{
|
|
name: 'Resume Matcher',
|
|
url: 'http://localhost:4023',
|
|
contentCheck: 'resume',
|
|
waitUntil: 'networkidle',
|
|
},
|
|
{
|
|
name: 'Apple Health',
|
|
url: 'http://localhost:4024',
|
|
contentCheck: 'apple-health-collector',
|
|
},
|
|
];
|
|
|
|
for (const svc of services) {
|
|
test(`${svc.name} (${svc.url}) loads successfully`, async ({ page }) => {
|
|
const response = await page.goto(svc.url, {
|
|
waitUntil: svc.waitUntil || 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
expect(response).not.toBeNull();
|
|
expect(response!.status()).toBeLessThan(400);
|
|
|
|
// Wait for SPA-rendered content to appear (handles Next.js, React, etc.)
|
|
await page
|
|
.waitForFunction(
|
|
(expected: string) => (document.body.textContent || '').toLowerCase().includes(expected),
|
|
svc.contentCheck.toLowerCase(),
|
|
{ timeout: 10000 }
|
|
)
|
|
.catch(() => {});
|
|
|
|
const body = await page.textContent('body').catch(() => '');
|
|
const title = await page.title().catch(() => '');
|
|
const combined = (body + ' ' + title).toLowerCase();
|
|
|
|
expect(
|
|
combined,
|
|
`${svc.name} should not show an error page`
|
|
).not.toContain('host validation failed');
|
|
expect(
|
|
combined,
|
|
`${svc.name} should not show a server error`
|
|
).not.toContain('internal server error');
|
|
expect(
|
|
combined,
|
|
`${svc.name} should contain expected content`
|
|
).toContain(svc.contentCheck.toLowerCase());
|
|
|
|
if (svc.titleCheck) {
|
|
expect(
|
|
title.toLowerCase(),
|
|
`${svc.name} should have expected title`
|
|
).toContain(svc.titleCheck.toLowerCase());
|
|
}
|
|
});
|
|
}
|