Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ node_modules/
web/.next/
web/out/

# Playwright
web/playwright-report/
web/test-results/

# OpenNext (Cloudflare) build output
web/.open-next/

Expand Down
733 changes: 443 additions & 290 deletions package-lock.json

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions web/e2e/pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, test } from '@playwright/test';
import { readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { allAgentSlugs } from '../lib/agents';
import { getAllDocSlugs, getAllLegacyDocSlugs } from '../lib/docs-nav';

const webDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const appDir = path.join(webDir, 'app');
const contentDir = path.join(webDir, 'content');

function filesIn(dir: string, filename: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const absolutePath = path.join(dir, entry.name);
if (entry.isDirectory()) return filesIn(absolutePath, filename);
return entry.name === filename ? [absolutePath] : [];
});
}

function mdxSlugs(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.mdx'))
.map((entry) => entry.name.slice(0, -4));
}

function staticPageRoutes(): string[] {
return filesIn(appDir, 'page.tsx')
.map((file) => path.relative(appDir, path.dirname(file)))
.filter((route) => !route.includes('['))
.map((route) => (route ? `/${route}` : '/'));
}

function pageRoutes(): string[] {
const currentDocs = getAllDocSlugs();
const legacyDocs = getAllLegacyDocSlugs();
const productDocs = ['agents', 'file', 'loop'].flatMap((product) =>
mdxSlugs(path.join(contentDir, `docs/${product}`)).map((slug) => `/docs/${product}/${slug}`),
);

const dynamicRoutes = [
...mdxSlugs(path.join(contentDir, 'blog')).map((slug) => `/blog/${slug}`),
...allAgentSlugs().map((slug) => `/agents/${slug}`),
...[...new Set([...currentDocs, ...legacyDocs])].map((slug) => `/docs/${slug}`),
...legacyDocs.map((slug) => `/docs/7.1.1/${slug}`),
...currentDocs.map((slug) => `/docs/8.0.0/${slug}`),
...legacyDocs.map((slug) => `/docs/pre-v8/${slug}`),
...productDocs,
'/openclaw/skill/invite/rk_live_smoke_test',
];

return [...new Set([...staticPageRoutes(), ...dynamicRoutes])].sort();
}

test.describe.configure({ mode: 'parallel' });

for (const route of pageRoutes()) {
test(`${route} renders visible page content`, async ({ page }) => {
const pageErrors: Error[] = [];
page.on('pageerror', (error) => pageErrors.push(error));

const response = await page.goto(route, { waitUntil: 'domcontentloaded' });
Comment thread
willwashburn marked this conversation as resolved.
await page.waitForLoadState('load');
// Let initial hydration and mount effects surface errors before asserting.
await page.waitForTimeout(100);

expect(response, 'navigation should return an HTTP response').not.toBeNull();
expect(response!.status(), 'page should not return an HTTP error').toBeLessThan(400);
const body = page.locator('body');
await expect(body).toBeVisible();
expect((await body.innerText()).trim().length, 'page should render meaningful visible text').toBeGreaterThan(100);
await expect(page).toHaveTitle(/\S+/);
await expect(page.locator('body')).not.toContainText('Application error');
expect(pageErrors, 'page should hydrate without uncaught browser errors').toEqual([]);
});
}
25 changes: 0 additions & 25 deletions web/middleware.ts

This file was deleted.

2 changes: 1 addition & 1 deletion web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
14 changes: 8 additions & 6 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"build": "next build --webpack",
"start": "next start",
"test": "vitest run",
"test:e2e": "playwright test",
"cf:build": "opennextjs-cloudflare build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && wrangler deploy"
Expand All @@ -25,21 +26,22 @@
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"lucide-react": "^1.0.1",
"next": "16.2.7",
"next": "16.2.10",
"postcss": "^8.5.8",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-icons": "^5.6.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@opennextjs/cloudflare": "^1.19.10",
"@opennextjs/cloudflare": "^1.20.1",
"@playwright/test": "^1.61.1",
"@types/node": "^22.19.3",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3",
"vitest": "^4.1.0",
"wrangler": "^4.91.0"
Expand Down
26 changes: 26 additions & 0 deletions web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI ? 'github' : 'line',
use: {
baseURL: 'http://localhost:3100',
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run build && npm run start -- -p 3100',
url: 'http://localhost:3100',
reuseExistingServer: !process.env.CI,
timeout: 180_000,
},
Comment thread
willwashburn marked this conversation as resolved.
});
Loading