chore: avb transition and automated testing setup
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 1m0s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m28s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-04-21 18:47:43 +02:00
parent 8427d348dd
commit e0803715a7
10 changed files with 530 additions and 10 deletions

26
.htmlvalidate.json Normal file
View File

@@ -0,0 +1,26 @@
{
"extends": ["html-validate:recommended", "html-validate:document"],
"rules": {
"require-sri": "off",
"meta-refresh": "off",
"heading-level": "warn",
"no-trailing-whitespace": "off",
"wcag/h37": "warn",
"no-inline-style": "off",
"svg-focusable": "off",
"attribute-boolean-style": "off",
"attr-case": "off",
"void-style": "off",
"no-implicit-button-type": "off",
"unique-landmark": "off",
"long-title": "off",
"valid-id": "off",
"element-required-attributes": "off",
"attribute-empty-style": "off",
"element-permitted-content": "off",
"element-required-content": "off",
"element-permitted-parent": "off",
"no-implicit-close": "off",
"close-order": "off"
}
}

21
.pa11yci.json Normal file
View File

@@ -0,0 +1,21 @@
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"ignore": [],
"timeout": 50000,
"wait": 1000,
"chromeLaunchConfig": {
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
},
"threshold": 25
},
"urls": [
"http://localhost:3000/",
"http://localhost:3000/ueber-uns",
"http://localhost:3000/kontakt",
"http://localhost:3000/impressum",
"http://localhost:3000/datenschutz",
"http://localhost:3000/agb"
]
}

View File

@@ -3,7 +3,7 @@ import fs from "fs";
import path from "path";
export default function AGB() {
const filePath = path.join(process.cwd(), "context/agbs.md");
const filePath = path.join(process.cwd(), "context/avbs.md");
const fileContent = fs.readFileSync(filePath, "utf8");
// Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs)
@@ -74,7 +74,7 @@ export default function AGB() {
<p className="text-slate-500 font-medium">{stand}</p>
</div>
<a
href="/assets/AGB MB Grid 3-2026.pdf"
href="/assets/AVB MB Grid 4-2026.pdf"
download
className="btn-primary !py-3 !px-6 flex items-center gap-2"
>

View File

@@ -1,12 +1,12 @@
Liefer- und Zahlungsbedingungen
Stand März 2026
Allgemeine Verkaufsbedingungen (AVB)
Stand April 2026
1. Allgemeines
Diese Liefer- und Zahlungsbedingungen (L&Z) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere L&Z gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren L&Z abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere L&Z gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen.
Diese Allgemeinen Verkaufsbedingungen (AVB) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere AVB gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren AVB abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere AVB gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen.
Nebenabreden, Vorbehalte, Änderungen, Ergänzungen usw. bedürfen zu ihrer Wirksamkeit unserer schriftlichen Bestätigung.
Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen L&Z nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin.
Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen AVB nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin.
2. Angebote
Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freibleibend; die Bestellung des Kunden ist als Angebot gemäß § 145 BGB zu qualifizieren.
@@ -73,7 +73,7 @@ Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des K
Wir haften unbeschränkt nur für Vorsatz und grobe Fahrlässigkeit sowie für Schäden aus einer Verletzung von Leben, Körper oder Gesundheit, die auf mindestens fahrlässiger Pflichtverletzung unsererseits oder unserer gesetzlichen Vertreter oder Erfüllungsgehilfen beruhen; ebenso haften wir unbeschränkt im Fall von uns übernommenen bzw. abgegebenen Garantien und Zusicherungen, sofern ein davon umfasster Mangel unsere Haftung auslöst sowie im Fall einer Haftung nach dem Produkthaftungsgesetz oder sonstigen Gefährdungshaftungstatbeständen. Im Fall sonstiger schuldhafter Verletzung wesentlicher Vertragspflichten („Kardinalpflichten“) ist unsere verbleibende Haftung auf den vertragstypischen vorhersehbaren Schaden beschränkt. Mangelfolgeschäden sowie entgangener Gewinn schließen wir grundsätzlich aus.
16. Kabeltrommeln
Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Bei Lagerzeiten über 3 Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließen wir aus. Zusätzlich die [PDF zum Download](/assets/AGB MB Grid 3-2026.pdf). Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen.
Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Bei Lagerzeiten über 3 Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließen wir aus. Zusätzlich die [PDF zum Download](/assets/AVB MB Grid 4-2026.pdf). Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen.
17. Technische Beratungsdienstleistungen
Die technische Unterstützung ersetzt weder die Fachplanung noch die Ausführungs- oder Prüfverantwortung des beauftragten Ingenieurbüros, Planers oder der ausführenden Fachfirma bzw. verantwortlichen Abteilung.
@@ -83,6 +83,6 @@ Alle Hinweise, Einschätzungen und Empfehlungen der MB Grid Solutions and Servic
18. Sonstiges
Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG). Gerichtsstand ist nach unserer Wahl Stuttgart, der Erfüllungsort der Lieferverpflichtung oder das für den Sitz des Bestellers zuständige Gericht, sofern der Besteller Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen ist oder keinen allgemeinen Gerichtsstand im Inland hat.
Mit der Veröffentlichung der vorliegenden L&Z im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
Mit der Veröffentlichung der vorliegenden AVB im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
Remshalden, 22.1.2026

View File

@@ -81,7 +81,7 @@
"legal": "Rechtliches",
"impressum": "Impressum",
"datenschutz": "Datenschutz",
"agb": "AGB",
"agb": "AVB",
"rights": "Alle Rechte vorbehalten.",
"madeWith": "Entwickelt mit",
"precision": "Präzision",

View File

@@ -1,6 +1,6 @@
{
"name": "mb-grid-solutions.com",
"version": "1.0.0",
"version": "1.3.13",
"type": "module",
"packageManager": "pnpm@10.18.3",
"scripts": {
@@ -17,6 +17,9 @@
"check:http": "tsx ./scripts/check-http.ts",
"check:apis": "tsx ./scripts/check-apis.ts",
"check:locale": "tsx ./scripts/check-locale.ts",
"check:html": "tsx ./scripts/check-html.ts",
"check:assets": "tsx ./scripts/check-broken-assets.ts",
"check:wcag": "tsx ./scripts/wcag-sitemap.ts",
"backup:db": "bash ./scripts/backup-db.sh"
},
"keywords": [],
@@ -72,12 +75,15 @@
"eslint": "^8.57.1",
"eslint-config-next": "15.1.6",
"happy-dom": "^20.6.1",
"html-validate": "^10.8.0",
"husky": "^9.1.7",
"jsdom": "^27.4.0",
"lint-staged": "^16.2.7",
"pa11y-ci": "^4.0.1",
"pino-pretty": "^13.1.3",
"postcss": "^8.5.6",
"prettier": "^3.5.0",
"puppeteer": "^24.37.3",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "^5.9.3",

Binary file not shown.

View File

@@ -0,0 +1,197 @@
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.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';
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`;
let urls: string[] = [];
try {
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const response = await axios.get(sitemapUrl, {
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
});
const $ = cheerio.load(response.data, { xmlMode: true });
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(/\/$/, '')))
.sort();
console.log(`✅ Found ${urls.length} target URLs.`);
if (urls.length > limit) {
console.log(
`⚠️ 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))];
}
} catch (err: any) {
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
process.exit(1);
}
// 2. Launch Headless Browser
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'],
});
const page = await browser.newPage();
// Inject Gatekeeper session bypassing auth screens
await page.setCookie({
name: 'mb_gatekeeper_session',
value: gatekeeperPassword,
domain: new URL(targetUrl).hostname,
path: '/',
httpOnly: true,
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 }> = [];
// Listen for unhandled exceptions natively in the page
page.on('pageerror', (err: any) => {
consoleErrorsList.push({
type: 'PAGE_ERROR',
error: err.message,
page: page.url(),
});
hasConsoleErrors = true;
});
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
page.on('console', (msg) => {
const type = msg.type();
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')
)
return;
consoleErrorsList.push({
type: type.toUpperCase(),
error: text,
page: page.url(),
});
hasConsoleErrors = true;
}
});
// Listen to ALL network responses
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')
) {
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)) {
brokenAssetsList.push({
url: response.url(),
status: status,
page: page.url(),
});
hasBrokenAssets = true;
}
}
});
// 3. Scan each page
for (let i = 0; i < urls.length; i++) {
const u = urls[i];
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 });
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
// Wait a tiny bit more for final lazy loads
await new Promise((r) => setTimeout(r, 1000));
} catch (err: any) {
console.error(`⚠️ Timeout or navigation error on ${u}: ${err.message}`);
// Don't fail the whole script just because one page timed out, but flag it
hasBrokenAssets = true;
}
}
await browser.close();
// 4. Report Results
if (hasBrokenAssets && brokenAssetsList.length > 0) {
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
console.table(brokenAssetsList);
}
if (hasConsoleErrors && consoleErrorsList.length > 0) {
console.error(`\n❌ FATAL: Console Errors/Warnings detected on the site!`);
console.table(consoleErrorsList);
}
if (hasBrokenAssets || hasConsoleErrors) {
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
process.exit(1);
} else {
console.log(
`\n🎉 SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken images or console errors!`,
);
process.exit(0);
}
}
main();

83
scripts/check-html.ts Normal file
View File

@@ -0,0 +1,83 @@
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';
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`;
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const response = await axios.get(sitemapUrl, {
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
validateStatus: (status) => status < 400,
});
const $ = cheerio.load(response.data, { xmlMode: true });
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(/\/$/, '')))
.sort();
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
if (urls.length === 0) {
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 });
fs.mkdirSync(outputDir, { recursive: true });
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
for (let i = 0; i < urls.length; i++) {
const u = urls[i];
try {
const res = await axios.get(u, {
headers: { Cookie: `mb_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`;
fs.writeFileSync(path.join(outputDir, filename), res.data);
} catch (err: any) {
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
throw new Error(`Failed to fetch page: ${u} - ${err.message}`);
}
}
console.log(`\n💻 Executing html-validate...`);
try {
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
console.log(`✅ HTML Validation passed perfectly!`);
} catch (e) {
console.error(`❌ HTML Validation found issues.`);
process.exit(1);
}
} catch (error: any) {
console.error(`\n❌ Error during HTML Validation:`, error.message);
process.exit(1);
} finally {
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
}
}
main();

187
scripts/wcag-sitemap.ts Normal file
View File

@@ -0,0 +1,187 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
/**
* WCAG Audit Script
*
* 1. Fetches sitemap.xml from the target URL
* 2. Extracts all URLs
* 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';
async function main() {
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
console.log(`📊 Limit: ${limit} pages\n`);
try {
// 1. Fetch Sitemap
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const response = await axios.get(sitemapUrl, {
headers: {
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
},
validateStatus: (status) => status < 400,
});
const $ = cheerio.load(response.data, { xmlMode: true });
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(/\/$/, '')))
.sort();
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
if (urls.length === 0) {
console.error('❌ No URLs found in sitemap. Is the site up?');
process.exit(1);
}
if (urls.length > limit) {
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 others = urls.filter((u) => !home.includes(u));
urls = [...home, ...others.slice(0, limit - home.length)];
}
console.log(`🧪 Pages to be tested:`);
urls.forEach((u) => console.log(` - ${u}`));
// 2. Prepare pa11y-ci config
const baseConfigPath = path.join(process.cwd(), '.pa11yci.json');
let baseConfig: any = { defaults: {} };
if (fs.existsSync(baseConfigPath)) {
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
}
// Extract domain for cookie
const urlObj = new URL(targetUrl);
const domain = urlObj.hostname;
// Update config with discovered URLs and gatekeeper cookie
const tempConfig = {
...baseConfig,
defaults: {
...baseConfig.defaults,
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
runners: ['axe'],
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
chromeLaunchConfig: {
...baseConfig.defaults?.chromeLaunchConfig,
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
args: [
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
'--no-sandbox',
'--disable-setuid-sandbox',
],
},
headers: {
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
},
timeout: 60000, // Increase timeout for slower pages
},
urls: urls,
};
// Create output directory
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');
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
// 3. Execute pa11y-ci
console.log(`\n💻 Executing pa11y-ci...`);
const pa11yCommand = `npx pa11y-ci --config .pa11yci/config.temp.json --reporter json > .pa11yci/report.json`;
try {
execSync(pa11yCommand, {
encoding: 'utf8',
stdio: 'inherit',
});
} catch (err: any) {
// pa11y-ci exits with non-zero if issues are found, which is expected
}
// 4. Summarize Results
if (fs.existsSync(reportPath)) {
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
console.log(`\n📊 WCAG Audit Summary:\n`);
const summaryTable = Object.keys(reportData.results).map((url) => {
const results = reportData.results[url];
// Results might have errors or just a top level message if it crashed
let errors = 0;
let warnings = 0;
let notices = 0;
if (Array.isArray(results)) {
// pa11y action execution errors come as objects with a message but no type
const actionErrors = results.filter((r: any) => !r.type && r.message).length;
errors = results.filter((r: any) => r.type === 'error').length + actionErrors;
warnings = results.filter((r: any) => r.type === 'warning').length;
notices = results.filter((r: any) => r.type === 'notice').length;
}
// Clean URL for display
const displayUrl = url.replace(targetUrl, '') || '/';
return {
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
Errors: errors,
Warnings: warnings,
Notices: notices,
Status: errors === 0 ? '✅' : '❌',
};
});
console.table(summaryTable);
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.`);
if (totalErrors > 0) {
console.log(` Total Errors discovered: ${totalErrors}`);
process.exitCode = 1;
}
}
console.log(`\n✨ WCAG Audit completed!`);
} catch (error: any) {
console.error(`\n❌ Error during WCAG Audit:`);
if (axios.isAxiosError(error)) {
console.error(`Status: ${error.response?.status}`);
console.error(`URL: ${error.config?.url}`);
} else {
console.error(error.message);
}
process.exit(1);
} finally {
// Clean up temp config file, keep report
const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json');
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
}
}
main();