blob: 2808dbd348173489bfb29762e0829d558e9e7644 [file]
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const {spawn, spawnSync} = require('child_process');
const {chromium} = require('playwright');
const repoRoot = path.resolve(__dirname, '..');
const buildDir = path.join(repoRoot, 'build');
const artifactDir = path.join(repoRoot, 'artifacts', 'playwright');
const port = Number(process.env.DOCUSAURUS_TEST_PORT || 4173);
const baseUrl = `http://127.0.0.1:${port}`;
function ensureBuild() {
if (fs.existsSync(path.join(buildDir, 'index.html'))) {
return;
}
const result = spawnSync('npm', ['run', 'build'], {cwd: repoRoot, stdio: 'inherit'});
if (result.status !== 0) {
process.exit(result.status || 1);
}
}
async function waitForServer(server) {
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
if (server.exitCode !== null) {
throw new Error(`Docusaurus server exited with code ${server.exitCode}`);
}
try {
const response = await fetch(baseUrl);
if (response.ok) {
return;
}
} catch {
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
throw new Error('Timed out waiting for Docusaurus test server');
}
async function expectVisible(locator, label) {
try {
await locator.first().waitFor({state: 'visible', timeout: 5000});
} catch {
throw new Error(`${label} is not visible`);
}
}
async function assertShell(page) {
await page.goto(`${baseUrl}/`, {waitUntil: 'networkidle'});
await expectVisible(page.locator('nav.navbar'), 'top navigation');
const asfDropdown = page.locator('.navbar__item.dropdown', {hasText: 'ASF'}).first();
await asfDropdown.hover();
await page.waitForTimeout(200);
const asfText = await asfDropdown.textContent();
for (const label of ['Foundation', 'License', 'Events', 'Privacy', 'Security', 'Sponsorship', 'Thanks', 'Code of Conduct']) {
if (!asfText.includes(label)) {
throw new Error(`ASF dropdown is missing ${label}`);
}
}
const footer = page.locator('footer.footer');
await footer.scrollIntoViewIfNeeded();
await expectVisible(footer.locator('img[alt*="Apache Software Foundation"]'), 'Apache/ASF footer logo');
const footerText = await footer.textContent();
for (const text of ['Copyright', 'Apache License, Version 2.0', 'trademarks', 'The Apache Software Foundation']) {
if (!footerText.includes(text)) {
throw new Error(`Footer is missing "${text}"`);
}
}
}
async function assertDocs(page) {
await page.goto(`${baseUrl}/docs/`, {waitUntil: 'networkidle'});
const navbar = page.locator('nav.navbar');
const navbarText = await navbar.textContent();
for (const label of ['Documentation', 'next', 'Stable (1.7.0)']) {
if (!navbarText.includes(label)) {
throw new Error(`Docs navbar is missing version label ${label}`);
}
}
await page.goto(`${baseUrl}/docs/quickstart/hugegraph/hugegraph-server/`, {waitUntil: 'networkidle'});
const sidebar = page.locator('aside.theme-doc-sidebar-container');
await expectVisible(sidebar, 'docs sidebar');
const quickStart = sidebar.getByText('Quick Start').first();
await expectVisible(quickStart, 'docs sidebar Quick Start item');
await page.goto(`${baseUrl}/docs/next/`, {waitUntil: 'networkidle'});
await expectVisible(page.getByText('This is unreleased documentation'), 'next docs unreleased banner');
}
async function assertCommunityPages(page) {
await page.goto(`${baseUrl}/team/`, {waitUntil: 'networkidle'});
await expectVisible(page.getByRole('heading', {name: 'Apache HugeGraph Team'}), 'team page heading');
await expectVisible(page.getByText('PMC Members').first(), 'team page PMC section');
await page.goto(`${baseUrl}/users/`, {waitUntil: 'networkidle'});
await expectVisible(page.getByRole('heading', {name: 'HugeGraph User Showcase'}), 'users page heading');
await expectVisible(page.getByText('Baidu').first(), 'users page case card');
}
async function assertMobileMenu(page) {
await page.setViewportSize({width: 390, height: 844});
await page.goto(`${baseUrl}/`, {waitUntil: 'networkidle'});
await page.locator('.navbar__toggle').click();
const sidebar = page.locator('.navbar-sidebar');
await expectVisible(sidebar, 'mobile navbar sidebar');
const text = await sidebar.textContent();
for (const label of ['Documentation', 'Blog', 'Community', 'Team', 'Users', 'ASF']) {
if (!text.includes(label)) {
throw new Error(`Mobile menu is missing ${label}`);
}
}
}
async function captureScreenshots(page) {
fs.mkdirSync(artifactDir, {recursive: true});
const viewports = [
['desktop', {width: 1440, height: 900}],
['mobile', {width: 390, height: 844}],
];
const pages = [
['homepage', '/'],
['docs-landing', '/docs/'],
['deep-docs', '/docs/quickstart/hugegraph/hugegraph-server/'],
['team', '/team/'],
['users', '/users/'],
];
for (const [viewportName, viewport] of viewports) {
await page.setViewportSize(viewport);
for (const [name, route] of pages) {
await page.goto(`${baseUrl}${route}`, {waitUntil: 'networkidle'});
await page.screenshot({
path: path.join(artifactDir, `${name}-${viewportName}.png`),
fullPage: false,
});
}
await page.goto(`${baseUrl}/`, {waitUntil: 'networkidle'});
await page.locator('footer.footer').scrollIntoViewIfNeeded();
await page.screenshot({
path: path.join(artifactDir, `footer-${viewportName}.png`),
fullPage: false,
});
}
}
async function main() {
ensureBuild();
const bin = path.join(repoRoot, 'node_modules', '.bin', 'docusaurus');
const server = spawn(bin, ['serve', '--host', '127.0.0.1', '--port', String(port)], {
cwd: repoRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});
server.stdout.on('data', (data) => process.stdout.write(data));
server.stderr.on('data', (data) => process.stderr.write(data));
try {
await waitForServer(server);
const browser = await chromium.launch();
const page = await browser.newPage({viewport: {width: 1440, height: 900}});
await assertShell(page);
await assertDocs(page);
await assertCommunityPages(page);
await assertMobileMenu(page);
await captureScreenshots(page);
await browser.close();
} finally {
server.kill('SIGTERM');
}
console.log(`UI validation OK. Screenshots: ${path.relative(repoRoot, artifactDir)}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});