diff --git a/frontend/scripts/axe-run.cjs b/frontend/scripts/axe-run.cjs index f7164b7..c620c8d 100644 --- a/frontend/scripts/axe-run.cjs +++ b/frontend/scripts/axe-run.cjs @@ -1,31 +1,50 @@ #!/usr/bin/env node /** - * CommonJS Axe + Puppeteer runner to support Node with type: module + * CommonJS Axe + Puppeteer runner to support Node with type: module. + * Launches a headless browser, injects axe-core, runs an accessibility audit, + * and saves the JSON report to the lighthouse/ output directory. + * + * Usage: node scripts/axe-run.cjs */ const fs = require("fs"); const path = require("path"); const puppeteer = require("puppeteer"); const axeSource = require("axe-core").source; -async function run(url) { +// Main runner — opens the URL, injects axe-core, and collects the audit result +const run = async (url) => { + // Launch headless browser with sandbox flags suitable for CI environments const browser = await puppeteer.launch({ args: ["--no-sandbox", "--disable-setuid-sandbox"], }); const page = await browser.newPage(); + + // Navigate to the target URL and wait until network is idle await page.goto(url, { waitUntil: "networkidle0" }); - // Inject axe + + // Inject axe-core library into the page context await page.evaluate(axeSource); - // Run axe with default options + + // Run the full axe accessibility audit and collect results const result = await page.evaluate(async () => await window.axe.run()); + // Ensure the output directory exists before writing the report const outDir = path.resolve(__dirname, "..", "lighthouse"); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + + // Generate a timestamped filename so reports don't overwrite each other const ts = new Date().toISOString().replace(/[:.]/g, "-"); const jsonPath = path.join(outDir, `axe-${ts}.json`); + + // Persist the full axe result as JSON for later review fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2)); await browser.close(); + + // Print summary to stdout console.log("Axe report saved to", jsonPath); console.log("Violations:", result.violations.length); + + // Log each violation with its impact level and affected nodes, then exit with code 2 if (result.violations.length > 0) { result.violations.forEach((v) => { console.log("\n", v.id, "-", v.impact, "-", v.help); @@ -33,13 +52,16 @@ async function run(url) { }); process.exit(2); } -} +}; +// Parse CLI arguments — require exactly one URL argument const args = process.argv.slice(2); if (args.length < 1) { console.error("Usage: node scripts/axe-run.cjs "); process.exit(1); } + +// Execute the runner and handle any unexpected errors run(args[0]).catch((err) => { console.error(err); process.exit(1); diff --git a/frontend/scripts/lighthouse-run.cjs b/frontend/scripts/lighthouse-run.cjs index 9fe1cc8..7455e5f 100644 --- a/frontend/scripts/lighthouse-run.cjs +++ b/frontend/scripts/lighthouse-run.cjs @@ -1,14 +1,22 @@ #!/usr/bin/env node -/* CommonJS version for environments with "type": "module" - Runs Lighthouse in headless Chrome using chrome-launcher -*/ +/** + * CommonJS Lighthouse runner for environments with "type": "module". + * Launches Chromium via chrome-launcher, runs Lighthouse against the given URL, + * and saves HTML + JSON reports to the lighthouse/ output directory. + * + * Usage: node scripts/lighthouse-run.cjs [--mobile|--desktop] + */ const fs = require("fs"); const path = require("path"); const lighthouseModule = require("lighthouse"); + +// Support both default and named exports depending on the installed version const lighthouse = lighthouseModule.default || lighthouseModule; const chromeLauncher = require("chrome-launcher"); -async function run(url, opts = {}) { +// Main runner — launches Chrome, runs Lighthouse, and saves reports +const run = async (url, opts = {}) => { + // Launch headless Chromium with flags suitable for CI and containerised environments const chrome = await chromeLauncher.launch({ chromeFlags: [ "--headless=new", @@ -20,21 +28,29 @@ async function run(url, opts = {}) { "--disable-features=IsolateOrigins,site-per-process", ], }); + + // Build Lighthouse options — default to desktop form factor if not specified const options = { port: chrome.port, output: "html", onlyCategories: ["performance", "accessibility", "best-practices", "seo"], emulatedFormFactor: opts.formFactor || "desktop", }; + console.log( `Running Lighthouse on ${url} (${options.emulatedFormFactor})...`, ); + + // Execute the Lighthouse audit and extract HTML + JSON reports const runnerResult = await lighthouse(url, options); const reportHtml = runnerResult.report; const reportJson = runnerResult.lhr; + // Ensure the output directory exists before writing reports const outDir = path.resolve(__dirname, "..", "lighthouse"); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + + // Generate timestamped filenames so reports don't overwrite each other const ts = new Date().toISOString().replace(/[:.]/g, "-"); const htmlPath = path.join( outDir, @@ -44,9 +60,12 @@ async function run(url, opts = {}) { outDir, `lighthouse-${options.emulatedFormFactor}-${ts}.json`, ); + + // Write both the human-readable HTML report and the machine-readable JSON fs.writeFileSync(htmlPath, reportHtml); fs.writeFileSync(jsonPath, JSON.stringify(reportJson, null, 2)); + // Print report paths and a compact category score summary console.log(`Saved reports: ${htmlPath} and ${jsonPath}`); console.log("----- Scores -----"); console.log( @@ -63,8 +82,9 @@ async function run(url, opts = {}) { ); await chrome.kill(); -} +}; +// Parse CLI arguments — require exactly one URL; form factor defaults to desktop const args = process.argv.slice(2); if (args.length < 1) { console.error( @@ -74,6 +94,8 @@ if (args.length < 1) { } const url = args[0]; const formFactor = args.includes("--mobile") ? "mobile" : "desktop"; + +// Execute the runner and handle any unexpected errors run(url, { formFactor }).catch((err) => { console.error(err); process.exit(1);