diff --git a/.gitea/workflows/qa-inline.yml b/.gitea/workflows/qa-inline.yml deleted file mode 100644 index fd90154..0000000 --- a/.gitea/workflows/qa-inline.yml +++ /dev/null @@ -1,233 +0,0 @@ -name: Nightly QA (Inline) - -on: - schedule: - - cron: '0 2 * * *' - push: - branches: - - main - paths: - - '.gitea/workflows/qa-inline.yml' - workflow_dispatch: - -jobs: - prepare: - name: πŸ—οΈ Prepare & Install - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - name: πŸ” Registry Auth - run: | - REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" - echo "@mintel:registry=https://$REGISTRY" > .npmrc - echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc - - name: πŸ” Diagnostics - run: | - echo "πŸ‘€ User: $(id)" - echo "πŸ“‚ Directory: $(pwd)" - echo "πŸ“‚ Disk Space:" - df -h - - name: Install dependencies - run: | - rm -rf .next .turbo node_modules || true - pnpm install --no-frozen-lockfile --reporter=append-only - - name: πŸ“¦ Archive dependencies - uses: actions/upload-artifact@v3 - with: - name: node_modules - path: node_modules - retention-days: 1 - - static: - name: πŸ” Static Analysis - needs: prepare - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: πŸ“₯ Restore dependencies - uses: actions/download-artifact@v3 - with: - name: node_modules - - name: 🌐 HTML Validation - env: - NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com' - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:html - - name: πŸ–ΌοΈ Asset Scan - env: - NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com' - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:assets - - accessibility: - name: β™Ώ Accessibility - needs: prepare - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: πŸ“₯ Restore dependencies - uses: actions/download-artifact@v3 - with: - name: node_modules - - name: πŸ” Install Chromium - run: | - apt-get update && apt-get install -y gnupg wget ca-certificates - CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME) - mkdir -p /etc/apt/keyrings - wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg - echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list - printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb - apt-get update && apt-get install -y --allow-downgrades chromium - ln -sf /usr/bin/chromium /usr/bin/google-chrome - - name: β™Ώ WCAG Scan - continue-on-error: true - env: - NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com' - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:wcag - - analysis: - name: πŸ§ͺ Maintenance & Links - needs: prepare - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: πŸ“₯ Restore dependencies - uses: actions/download-artifact@v3 - with: - name: node_modules - - name: πŸ“¦ Depcheck - continue-on-error: true - run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" - - name: πŸ”— Lychee Link Check - uses: lycheeverse/lychee-action@v2 - with: - args: --accept 200,204,429 --timeout 15 content/ app/ public/ - fail: true - - performance: - name: 🎭 Lighthouse - needs: prepare - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - - name: πŸ“₯ Restore dependencies - uses: actions/download-artifact@v3 - with: - name: node_modules - - name: πŸ” Install Chromium - run: | - apt-get update && apt-get install -y gnupg wget ca-certificates - CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME) - mkdir -p /etc/apt/keyrings - wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg - echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list - printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb - apt-get update && apt-get install -y --allow-downgrades chromium - ln -sf /usr/bin/chromium /usr/bin/google-chrome - - name: 🎭 LHCI Desktop - env: - LHCI_URL: 'https://testing.mb-grid-solutions.com' - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run pagespeed:test -- --collect.settings.preset=desktop - - name: πŸ“± LHCI Mobile - env: - LHCI_URL: 'https://testing.mb-grid-solutions.com' - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run pagespeed:test -- --collect.settings.preset=mobile - - notifications: - name: πŸ”” Notify - needs: [prepare, static, accessibility, analysis, performance] - if: always() - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: πŸ”” Gotify - shell: bash - run: | - PREPARE="${{ needs.prepare.result }}" - STATIC="${{ needs.static.result }}" - A11Y="${{ needs.accessibility.result }}" - ANALYSIS="${{ needs.analysis.result }}" - PERF="${{ needs.performance.result }}" - - PROJECT='mb-grid-solutions' - URL='https://testing.mb-grid-solutions.com' - - if [[ "$PREPARE" != "success" || "$STATIC" != "success" || "$PERF" != "success" ]]; then - PRIORITY=8 - EMOJI="🚨" - STATUS_LINE="Nightly QA Failed! Action required." - else - PRIORITY=2 - EMOJI="βœ…" - STATUS_LINE="Nightly QA Passed." - fi - - TITLE="$EMOJI $PROJECT Nightly QA" - MESSAGE="$STATUS_LINE - Prepare: $PREPARE | Static: $STATIC | A11y: $A11Y - Analysis: $ANALYSIS | Perf: $PERF - $URL" - - curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=$TITLE" \ - -F "message=$MESSAGE" \ - -F "priority=$PRIORITY" || true diff --git a/.gitea/workflows/qa.yml b/.gitea/workflows/qa.yml index 548c1eb..7b1a4a7 100644 --- a/.gitea/workflows/qa.yml +++ b/.gitea/workflows/qa.yml @@ -14,7 +14,7 @@ on: jobs: call-qa-workflow: # Nightly QA: Trigger run with diagnostics. - uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@3f16828bdf69fae5b10ac502d66cd02d959ca813 + uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main with: TARGET_URL: 'https://testing.mb-grid-solutions.com' PROJECT_NAME: 'mb-grid-solutions' diff --git a/scripts/check-broken-assets.ts b/scripts/check-broken-assets.ts index 6724c4c..ab6f084 100644 --- a/scripts/check-broken-assets.ts +++ b/scripts/check-broken-assets.ts @@ -1,38 +1,40 @@ -import puppeteer, { HTTPResponse } from 'puppeteer'; -import axios from 'axios'; -import * as cheerio from 'cheerio'; +import puppeteer, { HTTPResponse } from "puppeteer"; +import axios from "axios"; +import * as cheerio from "cheerio"; const targetUrl = - process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) || + process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) || process.env.NEXT_PUBLIC_BASE_URL || - 'http://localhost:3000'; -const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20; -const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid'; + "http://localhost:3000"; +const limit = process.env.ASSET_CHECK_LIMIT + ? parseInt(process.env.ASSET_CHECK_LIMIT) + : 20; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid"; async function main() { console.log(`\nπŸš€ Starting Strict Asset Integrity Check for: ${targetUrl}`); console.log(`πŸ“Š Limit: ${limit} pages\n`); // 1. Fetch Sitemap to discover all routes - const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; + const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; let urls: string[] = []; try { console.log(`πŸ“₯ Fetching sitemap from ${sitemapUrl}...`); const response = await axios.get(sitemapUrl, { - headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` }, + headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` }, }); const $ = cheerio.load(response.data, { xmlMode: true }); - urls = $('url loc') + urls = $("url loc") .map((i, el) => $(el).text()) .get(); // Normalize to target URL instance const urlPattern = /https?:\/\/[^\/]+/; urls = [...new Set(urls)] - .filter((u) => u.startsWith('http')) - .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) + .filter((u) => u.startsWith("http")) + .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ""))) .sort(); console.log(`βœ… Found ${urls.length} target URLs.`); @@ -42,10 +44,16 @@ async function main() { `⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`, ); // Simplify selection: home pages + a slice of the rest - const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl); - const homeEN = urls.filter((u) => u.endsWith('/en')); - const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u)); - urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))]; + const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl); + const homeEN = urls.filter((u) => u.endsWith("/en")); + const others = urls.filter( + (u) => !homeEN.includes(u) && !homeDE.includes(u), + ); + urls = [ + ...homeDE, + ...homeEN, + ...others.slice(0, limit - (homeEN.length + homeDE.length)), + ]; } } catch (err: unknown) { const errorBody = err instanceof Error ? err.message : String(err); @@ -57,32 +65,44 @@ async function main() { console.log(`\nπŸ•·οΈ Launching Puppeteer Headless Engine...`); const browser = await puppeteer.launch({ headless: true, - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + executablePath: + process.env.PUPPETEER_EXECUTABLE_PATH || + process.env.CHROME_PATH || + undefined, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + ], }); const page = await browser.newPage(); // Inject Gatekeeper session bypassing auth screens await page.setCookie({ - name: 'mb_gatekeeper_session', + name: "mintel_gatekeeper_session", value: gatekeeperPassword, domain: new URL(targetUrl).hostname, - path: '/', + path: "/", httpOnly: true, - secure: targetUrl.startsWith('https://'), + secure: targetUrl.startsWith("https://"), }); let hasBrokenAssets = false; let hasConsoleErrors = false; - const brokenAssetsList: Array<{ url: string; status: number; page: string }> = []; - const consoleErrorsList: Array<{ type: string; error: string; page: string }> = []; + const brokenAssetsList: Array<{ url: string; status: number; page: string }> = + []; + const consoleErrorsList: Array<{ + type: string; + error: string; + page: string; + }> = []; // Listen for unhandled exceptions natively in the page - page.on('pageerror', (err: unknown) => { + page.on("pageerror", (err: unknown) => { const errorMessage = err instanceof Error ? err.message : String(err); consoleErrorsList.push({ - type: 'PAGE_ERROR', + type: "PAGE_ERROR", error: errorMessage, page: page.url(), }); @@ -91,18 +111,18 @@ async function main() { // Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks) // eslint-disable-next-line @typescript-eslint/no-explicit-any - page.on('console', (msg: any) => { + page.on("console", (msg: any) => { const type = msg.type(); - if (type === 'error' || type === 'warn') { + if (type === "error" || type === "warn") { const text = msg.text(); // Exclude common browser extension noise or third party tracker warnings if ( - text.includes('google-analytics') || - text.includes('googletagmanager') || - text.includes('SES Removing unpermitted intrinsics') || - text.includes('Third-party cookie will be blocked') || - text.includes('Fast Refresh') + text.includes("google-analytics") || + text.includes("googletagmanager") || + text.includes("SES Removing unpermitted intrinsics") || + text.includes("Third-party cookie will be blocked") || + text.includes("Fast Refresh") ) return; @@ -116,19 +136,19 @@ async function main() { }); // Listen to ALL network responses - page.on('response', (response: HTTPResponse) => { + page.on("response", (response: HTTPResponse) => { const status = response.status(); // Catch classic 404s and 500s on ANY fetch/image/script if ( status >= 400 && status !== 999 && - !response.url().includes('google-analytics') && - !response.url().includes('googletagmanager') + !response.url().includes("google-analytics") && + !response.url().includes("googletagmanager") ) { const type = response.request().resourceType(); // We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s. - if (['image', 'script', 'stylesheet', 'fetch', 'xhr'].includes(type)) { + if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) { brokenAssetsList.push({ url: response.url(), status: status, @@ -145,7 +165,7 @@ async function main() { console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`); try { // Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger - await page.goto(u, { waitUntil: 'networkidle0', timeout: 30000 }); + await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 }); // Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images await page.evaluate(async () => { @@ -178,7 +198,9 @@ async function main() { // 4. Report Results if (hasBrokenAssets && brokenAssetsList.length > 0) { - console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`); + console.error( + `\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`, + ); console.table(brokenAssetsList); } @@ -188,7 +210,9 @@ async function main() { } if (hasBrokenAssets || hasConsoleErrors) { - console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`); + console.error( + `\n🚨 The CI build will now fail to prevent bad code from reaching production.`, + ); process.exit(1); } else { console.log( diff --git a/scripts/check-html.ts b/scripts/check-html.ts index e00d96b..860fa59 100644 --- a/scripts/check-html.ts +++ b/scripts/check-html.ts @@ -1,45 +1,49 @@ -import axios from 'axios'; -import * as cheerio from 'cheerio'; -import * as fs from 'fs'; -import * as path from 'path'; -import { execSync } from 'child_process'; +import axios from "axios"; +import * as cheerio from "cheerio"; +import * as fs from "fs"; +import * as path from "path"; +import { execSync } from "child_process"; -const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; -const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid'; +const targetUrl = + process.argv[2] || + process.env.NEXT_PUBLIC_BASE_URL || + "http://localhost:3000"; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid"; async function main() { console.log(`\nπŸš€ Starting HTML Validation for: ${targetUrl}`); console.log(`πŸ“Š Limit: None (Full Sitemap)\n`); try { - const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; + const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; console.log(`πŸ“₯ Fetching sitemap from ${sitemapUrl}...`); const response = await axios.get(sitemapUrl, { - headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` }, + headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` }, validateStatus: (status) => status < 400, }); const $ = cheerio.load(response.data, { xmlMode: true }); - let urls = $('url loc') + let urls = $("url loc") .map((i, el) => $(el).text()) .get(); const urlPattern = /https?:\/\/[^\/]+/; urls = [...new Set(urls)] - .filter((u) => u.startsWith('http')) - .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) + .filter((u) => u.startsWith("http")) + .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ""))) .sort(); console.log(`βœ… Found ${urls.length} URLs in sitemap.`); if (urls.length === 0) { - console.error('❌ No URLs found in sitemap. Is the site up?'); + console.error("❌ No URLs found in sitemap. Is the site up?"); process.exit(1); } - const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp'); - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true }); + const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp"); + if (fs.existsSync(outputDir)) + fs.rmSync(outputDir, { recursive: true, force: true }); fs.mkdirSync(outputDir, { recursive: true }); console.log(`πŸ“₯ Fetching HTML for ${urls.length} pages...`); @@ -47,14 +51,19 @@ async function main() { const u = urls[i]; try { const res = await axios.get(u, { - headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` }, + headers: { + Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`, + }, validateStatus: (status) => status < 400, }); // Generate a safe filename that retains URL information const urlStr = new URL(u); - const safePath = (urlStr.pathname + urlStr.search).replace(/[^a-zA-Z0-9]/g, '_'); - const filename = `${safePath || 'index'}.html`; + const safePath = (urlStr.pathname + urlStr.search).replace( + /[^a-zA-Z0-9]/g, + "_", + ); + const filename = `${safePath || "index"}.html`; fs.writeFileSync(path.join(outputDir, filename), res.data); } catch (err: unknown) { @@ -66,7 +75,9 @@ async function main() { console.log(`\nπŸ’» Executing html-validate...`); try { - execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' }); + execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { + stdio: "inherit", + }); console.log(`βœ… HTML Validation passed perfectly!`); } catch { console.error(`❌ HTML Validation found issues.`); @@ -77,8 +88,9 @@ async function main() { console.error(`\n❌ Error during HTML Validation:`, errorBody); process.exit(1); } finally { - const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp'); - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true }); + const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp"); + if (fs.existsSync(outputDir)) + fs.rmSync(outputDir, { recursive: true, force: true }); } } diff --git a/scripts/wcag-sitemap.ts b/scripts/wcag-sitemap.ts index 1d1e98e..f9d340e 100644 --- a/scripts/wcag-sitemap.ts +++ b/scripts/wcag-sitemap.ts @@ -1,9 +1,9 @@ -import axios from 'axios'; -import * as cheerio from 'cheerio'; -import { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { AxiosError } from 'axios'; +import axios from "axios"; +import * as cheerio from "cheerio"; +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { AxiosError } from "axios"; interface Pa11yConfig { defaults: { @@ -36,9 +36,14 @@ interface Pa11yResult { * 3. Runs pa11y-ci on those URLs */ -const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; -const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; -const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid'; +const targetUrl = + process.argv[2] || + process.env.NEXT_PUBLIC_BASE_URL || + "http://localhost:3000"; +const limit = process.env.PAGESPEED_LIMIT + ? parseInt(process.env.PAGESPEED_LIMIT) + : 20; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid"; async function main() { console.log(`\nπŸš€ Starting WCAG Audit for: ${targetUrl}`); @@ -46,32 +51,32 @@ async function main() { try { // 1. Fetch Sitemap - const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; + const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; console.log(`πŸ“₯ Fetching sitemap from ${sitemapUrl}...`); const response = await axios.get(sitemapUrl, { headers: { - Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`, + Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`, }, validateStatus: (status) => status < 400, }); const $ = cheerio.load(response.data, { xmlMode: true }); - let urls = $('url loc') + let urls = $("url loc") .map((i, el) => $(el).text()) .get(); // Cleanup, filter and normalize domains to targetUrl const urlPattern = /https?:\/\/[^\/]+/; urls = [...new Set(urls)] - .filter((u) => u.startsWith('http')) - .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) + .filter((u) => u.startsWith("http")) + .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ""))) .sort(); console.log(`βœ… Found ${urls.length} URLs in sitemap.`); if (urls.length === 0) { - console.error('❌ No URLs found in sitemap. Is the site up?'); + console.error("❌ No URLs found in sitemap. Is the site up?"); process.exit(1); } @@ -79,7 +84,9 @@ async function main() { console.log( `⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`, ); - const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl); + const home = urls.filter( + (u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl, + ); const others = urls.filter((u) => !home.includes(u)); urls = [...home, ...others.slice(0, limit - home.length)]; } @@ -88,10 +95,10 @@ async function main() { urls.forEach((u) => console.log(` - ${u}`)); // 2. Prepare pa11y-ci config - const baseConfigPath = path.join(process.cwd(), '.pa11yci.json'); + const baseConfigPath = path.join(process.cwd(), ".pa11yci.json"); let baseConfig: Pa11yConfig = { defaults: {} }; if (fs.existsSync(baseConfigPath)) { - baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8')); + baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8")); } // Extract domain for cookie (not currently used) @@ -105,19 +112,21 @@ async function main() { defaults: { ...baseConfig.defaults, threshold: 0, // Force threshold to 0 so all errors are shown in JSON - runners: ['axe'], - ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'], + runners: ["axe"], + ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"], chromeLaunchConfig: { ...baseConfig.defaults?.chromeLaunchConfig, - ...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}), + ...(process.env.CHROME_PATH + ? { executablePath: process.env.CHROME_PATH } + : {}), args: [ ...(baseConfig.defaults?.chromeLaunchConfig?.args || []), - '--no-sandbox', - '--disable-setuid-sandbox', + "--no-sandbox", + "--disable-setuid-sandbox", ], }, headers: { - Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`, + Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`, }, timeout: 60000, // Increase timeout for slower pages }, @@ -125,13 +134,13 @@ async function main() { }; // Create output directory - const outputDir = path.join(process.cwd(), '.pa11yci'); + const outputDir = path.join(process.cwd(), ".pa11yci"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } - const tempConfigPath = path.join(outputDir, 'config.temp.json'); - const reportPath = path.join(outputDir, 'report.json'); + const tempConfigPath = path.join(outputDir, "config.temp.json"); + const reportPath = path.join(outputDir, "report.json"); fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2)); // 3. Execute pa11y-ci @@ -140,8 +149,8 @@ async function main() { try { execSync(pa11yCommand, { - encoding: 'utf8', - stdio: 'inherit', + encoding: "utf8", + stdio: "inherit", }); } catch { // pa11y-ci exits with non-zero if issues are found, which is expected @@ -149,7 +158,7 @@ async function main() { // 4. Summarize Results if (fs.existsSync(reportPath)) { - const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8')); + const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8")); console.log(`\nπŸ“Š WCAG Audit Summary:\n`); const summaryTable = Object.keys(reportData.results).map((url) => { @@ -161,31 +170,47 @@ async function main() { if (Array.isArray(results)) { // pa11y action execution errors come as objects with a message but no type - const actionErrors = results.filter((r: Pa11yResult) => !r.type && r.message).length; - errors = results.filter((r: Pa11yResult) => r.type === 'error').length + actionErrors; - warnings = results.filter((r: Pa11yResult) => r.type === 'warning').length; - notices = results.filter((r: Pa11yResult) => r.type === 'notice').length; + const actionErrors = results.filter( + (r: Pa11yResult) => !r.type && r.message, + ).length; + errors = + results.filter((r: Pa11yResult) => r.type === "error").length + + actionErrors; + warnings = results.filter( + (r: Pa11yResult) => r.type === "warning", + ).length; + notices = results.filter( + (r: Pa11yResult) => r.type === "notice", + ).length; } // Clean URL for display - const displayUrl = url.replace(targetUrl, '') || '/'; + const displayUrl = url.replace(targetUrl, "") || "/"; return { - URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl, + URL: + displayUrl.length > 50 + ? displayUrl.substring(0, 47) + "..." + : displayUrl, Errors: errors, Warnings: warnings, Notices: notices, - Status: errors === 0 ? 'βœ…' : '❌', + Status: errors === 0 ? "βœ…" : "❌", }; }); console.table(summaryTable); - const totalErrors = summaryTable.reduce((acc, curr) => acc + curr.Errors, 0); + const totalErrors = summaryTable.reduce( + (acc, curr) => acc + curr.Errors, + 0, + ); const totalPages = summaryTable.length; const cleanPages = summaryTable.filter((p) => p.Errors === 0).length; - console.log(`\nπŸ“ˆ Result: ${cleanPages}/${totalPages} pages are error-free.`); + console.log( + `\nπŸ“ˆ Result: ${cleanPages}/${totalPages} pages are error-free.`, + ); if (totalErrors > 0) { console.log(` Total Errors discovered: ${totalErrors}`); process.exitCode = 1; @@ -205,7 +230,10 @@ async function main() { process.exit(1); } finally { // Clean up temp config file, keep report - const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json'); + const tempConfigPath = path.join( + process.cwd(), + ".pa11yci/config.temp.json", + ); if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath); } } diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..3950878 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,76 @@ + WARN  Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN} + WARN  Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN} + +> mb-grid-solutions.com@1.3.20 test /Volumes/Alpha SSD/Coding/mb-grid-solutions.com +> vitest run + + + RUN v4.0.18 /Volumes/Alpha SSD/Coding/mb-grid-solutions.com + + ❯ tests/api-contact.test.ts (0 test) + βœ“ lib/env.test.ts (6 tests) 4ms +DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled. + at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27) + at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23) + at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45) +DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled. + at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27) + at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23) + at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45) +DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled. + at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27) + at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23) + at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58) + at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45) + βœ“ tests/contact.test.tsx (3 tests) 148ms + βœ“ tests/home.test.tsx (3 tests) 123ms + +⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/api-contact.test.ts [ tests/api-contact.test.ts ] +Error: Failed to resolve import "@payload-config" from "app/api/contact/route.ts". Does the file exist? + Plugin: vite:import-analysis + File: /Volumes/Alpha SSD/Coding/mb-grid-solutions.com/app/api/contact/route.ts:3:26 + 1 | import { NextResponse } from "next/server"; + 2 | import { getPayload } from "payload"; + 3 | import configPromise from "@payload-config"; + | ^ + 4 | import { getServerAppServices } from "@/lib/services/create-services.server"; + 5 | import { + ❯ TransformPluginContext._formatLog ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28999:43 + ❯ TransformPluginContext.error ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28996:14 + ❯ normalizeUrl ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27119:18 + ❯ ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27177:32 + ❯ TransformPluginContext.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27145:4 + ❯ EnvironmentPluginContainer.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28797:14 + ❯ loadAndTransform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22670:26 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + + Test Files 1 failed | 3 passed (4) + Tests 12 passed (12) + Start at 14:27:09 + Duration 1.47s (transform 453ms, setup 660ms, import 1.35s, tests 275ms, environment 1.52s) + + ELIFECYCLE  Test failed. See above for more details.