Files
wiki/skills/ui-test/scripts/run_tests.js
2026-05-13 00:30:02 +03:00

197 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
// run_tests.js — UI test runner using Playwright + Chromium headless shell
const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');
const CHROMIUM_PATH = path.join(
process.env.HOME,
'.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell'
);
const LD_LIBRARY_PATH = [
path.join(process.env.HOME, 'chromium-libs/libs/usr/lib/x86_64-linux-gnu'),
path.join(process.env.HOME, 'chromium-libs/libs/lib/x86_64-linux-gnu')
].join(':');
const { parseTestCases } = require('./parse_testcases.js');
// Parse args
const testCasesFile = process.argv[2];
const outputDir = process.argv[3];
if (!testCasesFile || !outputDir) {
console.error('Usage: node run_tests.js <test-cases.md> <output-dir>');
process.exit(1);
}
function loadTestCases(filePath) {
const content = fs.readFileSync(path.resolve(filePath), 'utf8');
return parseTestCases(content);
}
async function launchBrowser(viewport) {
const browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
env: { ...process.env, LD_LIBRARY_PATH }
});
const context = await browser.newContext({
viewport: viewport === 'mobile'
? { width: 375, height: 812 }
: { width: 1280, height: 720 },
deviceScaleFactor: viewport === 'mobile' ? 2 : 1,
isMobile: viewport === 'mobile',
userAgent: viewport === 'mobile'
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'
: undefined
});
return { browser, context };
}
async function executeStep(page, step, screenshotDir, testId, viewportLabel) {
const prefix = `${testId}-${viewportLabel}`;
switch (step.action) {
case 'navigate':
await page.goto(step.value, { waitUntil: 'networkidle', timeout: 30000 });
break;
case 'wait':
await page.waitForTimeout(typeof step.value === 'number' ? step.value : parseInt(step.value));
break;
case 'click':
try {
await page.click(step.value, { timeout: 5000 });
} catch (e) {
return { error: `Click failed on "${step.value}": ${e.message.slice(0, 100)}` };
}
break;
case 'scroll':
await page.evaluate((px) => window.scrollBy(0, parseInt(px)), String(step.value));
break;
case 'screenshot': {
const filename = `${prefix}-${step.value}.png`;
await page.screenshot({ path: path.join(screenshotDir, filename), fullPage: false });
return { screenshot: filename };
}
case 'check-visual': {
const filename = `${prefix}-check-${Date.now()}.png`;
await page.screenshot({ path: path.join(screenshotDir, filename), fullPage: false });
return { screenshot: filename, checkDescription: step.value };
}
case 'resize': {
const [w, h] = step.value.split('x').map(Number);
await page.setViewportSize({ width: w, height: h });
break;
}
default:
return { warning: `Unknown action: ${step.action}` };
}
return null;
}
async function runTest(test, viewportLabel, screenshotDir) {
const result = {
id: test.id,
name: test.name,
viewport: viewportLabel,
status: 'completed',
screenshots: [],
checks: [],
errors: []
};
let browser, context;
try {
({ browser, context } = await launchBrowser(viewportLabel));
const page = await context.newPage();
for (const step of test.steps) {
const stepResult = await executeStep(page, step, screenshotDir, test.id, viewportLabel);
if (stepResult) {
if (stepResult.error) {
result.errors.push(stepResult.error);
// Continue with remaining steps
}
if (stepResult.screenshot) {
result.screenshots.push(stepResult.screenshot);
if (stepResult.checkDescription) {
result.checks.push({
description: stepResult.checkDescription,
screenshot: stepResult.screenshot
});
}
}
}
}
if (result.errors.length > 0) {
result.status = 'completed_with_errors';
}
} catch (e) {
result.status = 'failed';
result.errors.push(e.message.slice(0, 200));
} finally {
if (browser) await browser.close();
}
return result;
}
async function main() {
// Load test cases
const tests = loadTestCases(testCasesFile);
if (tests.length === 0) {
console.error('No UI test cases found in', testCasesFile);
process.exit(1);
}
console.log(`Found ${tests.length} UI test(s) in ${path.basename(testCasesFile)}`);
// Create output dirs
const screenshotDir = path.join(outputDir, 'screenshots');
fs.mkdirSync(screenshotDir, { recursive: true });
const allResults = [];
for (const test of tests) {
const viewports = test.viewport === 'both'
? ['desktop', 'mobile']
: [test.viewport];
for (const vp of viewports) {
console.log(` Running: ${test.id}${test.name} [${vp}]`);
const result = await runTest(test, vp, screenshotDir);
allResults.push(result);
const icon = result.status === 'completed' ? '✓' : result.status === 'completed_with_errors' ? '⚠' : '✗';
console.log(` ${icon} ${result.status}${result.screenshots.length} screenshot(s)`);
}
}
// Write results.json
const report = {
timestamp: new Date().toISOString(),
testFile: path.basename(testCasesFile),
results: allResults
};
const resultsPath = path.join(outputDir, 'results.json');
fs.writeFileSync(resultsPath, JSON.stringify(report, null, 2));
console.log(`\nResults saved to: ${resultsPath}`);
console.log(`Screenshots: ${screenshotDir}/`);
// Summary
const completed = allResults.filter(r => r.status === 'completed').length;
const withErrors = allResults.filter(r => r.status === 'completed_with_errors').length;
const failed = allResults.filter(r => r.status === 'failed').length;
console.log(`\nSummary: ${allResults.length} runs | ✓ ${completed} | ⚠ ${withErrors} | ✗ ${failed}`);
if (failed > 0) process.exit(1);
}
main().catch(e => {
console.error('Fatal error:', e.message);
process.exit(1);
});