chore(qa): restore reusable workflow with Gitea compatibility fixes
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m34s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m34s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
This commit is contained in:
@@ -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
|
|
||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
call-qa-workflow:
|
call-qa-workflow:
|
||||||
# Nightly QA: Trigger run with diagnostics.
|
# 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:
|
with:
|
||||||
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
||||||
PROJECT_NAME: 'mb-grid-solutions'
|
PROJECT_NAME: 'mb-grid-solutions'
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
import puppeteer, { HTTPResponse } from 'puppeteer';
|
import puppeteer, { HTTPResponse } from "puppeteer";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
const targetUrl =
|
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 ||
|
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||||
'http://localhost:3000';
|
"http://localhost:3000";
|
||||||
const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20;
|
const limit = process.env.ASSET_CHECK_LIMIT
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
? parseInt(process.env.ASSET_CHECK_LIMIT)
|
||||||
|
: 20;
|
||||||
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
||||||
console.log(`📊 Limit: ${limit} pages\n`);
|
console.log(`📊 Limit: ${limit} pages\n`);
|
||||||
|
|
||||||
// 1. Fetch Sitemap to discover all routes
|
// 1. Fetch Sitemap to discover all routes
|
||||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||||
let urls: string[] = [];
|
let urls: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||||
const response = await axios.get(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 });
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||||
urls = $('url loc')
|
urls = $("url loc")
|
||||||
.map((i, el) => $(el).text())
|
.map((i, el) => $(el).text())
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
// Normalize to target URL instance
|
// Normalize to target URL instance
|
||||||
const urlPattern = /https?:\/\/[^\/]+/;
|
const urlPattern = /https?:\/\/[^\/]+/;
|
||||||
urls = [...new Set(urls)]
|
urls = [...new Set(urls)]
|
||||||
.filter((u) => u.startsWith('http'))
|
.filter((u) => u.startsWith("http"))
|
||||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||||
@@ -42,10 +44,16 @@ async function main() {
|
|||||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||||
);
|
);
|
||||||
// Simplify selection: home pages + a slice of the rest
|
// Simplify selection: home pages + a slice of the rest
|
||||||
const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl);
|
const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl);
|
||||||
const homeEN = urls.filter((u) => u.endsWith('/en'));
|
const homeEN = urls.filter((u) => u.endsWith("/en"));
|
||||||
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
const others = urls.filter(
|
||||||
urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
(u) => !homeEN.includes(u) && !homeDE.includes(u),
|
||||||
|
);
|
||||||
|
urls = [
|
||||||
|
...homeDE,
|
||||||
|
...homeEN,
|
||||||
|
...others.slice(0, limit - (homeEN.length + homeDE.length)),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errorBody = err instanceof Error ? err.message : String(err);
|
const errorBody = err instanceof Error ? err.message : String(err);
|
||||||
@@ -57,32 +65,44 @@ async function main() {
|
|||||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
executablePath:
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
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();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
// Inject Gatekeeper session bypassing auth screens
|
// Inject Gatekeeper session bypassing auth screens
|
||||||
await page.setCookie({
|
await page.setCookie({
|
||||||
name: 'mb_gatekeeper_session',
|
name: "mintel_gatekeeper_session",
|
||||||
value: gatekeeperPassword,
|
value: gatekeeperPassword,
|
||||||
domain: new URL(targetUrl).hostname,
|
domain: new URL(targetUrl).hostname,
|
||||||
path: '/',
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: targetUrl.startsWith('https://'),
|
secure: targetUrl.startsWith("https://"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasBrokenAssets = false;
|
let hasBrokenAssets = false;
|
||||||
let hasConsoleErrors = false;
|
let hasConsoleErrors = false;
|
||||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> = [];
|
const brokenAssetsList: Array<{ url: string; status: number; page: string }> =
|
||||||
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
[];
|
||||||
|
const consoleErrorsList: Array<{
|
||||||
|
type: string;
|
||||||
|
error: string;
|
||||||
|
page: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
// Listen for unhandled exceptions natively in the page
|
// 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);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
consoleErrorsList.push({
|
consoleErrorsList.push({
|
||||||
type: 'PAGE_ERROR',
|
type: "PAGE_ERROR",
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
page: page.url(),
|
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)
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
page.on('console', (msg: any) => {
|
page.on("console", (msg: any) => {
|
||||||
const type = msg.type();
|
const type = msg.type();
|
||||||
if (type === 'error' || type === 'warn') {
|
if (type === "error" || type === "warn") {
|
||||||
const text = msg.text();
|
const text = msg.text();
|
||||||
|
|
||||||
// Exclude common browser extension noise or third party tracker warnings
|
// Exclude common browser extension noise or third party tracker warnings
|
||||||
if (
|
if (
|
||||||
text.includes('google-analytics') ||
|
text.includes("google-analytics") ||
|
||||||
text.includes('googletagmanager') ||
|
text.includes("googletagmanager") ||
|
||||||
text.includes('SES Removing unpermitted intrinsics') ||
|
text.includes("SES Removing unpermitted intrinsics") ||
|
||||||
text.includes('Third-party cookie will be blocked') ||
|
text.includes("Third-party cookie will be blocked") ||
|
||||||
text.includes('Fast Refresh')
|
text.includes("Fast Refresh")
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -116,19 +136,19 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Listen to ALL network responses
|
// Listen to ALL network responses
|
||||||
page.on('response', (response: HTTPResponse) => {
|
page.on("response", (response: HTTPResponse) => {
|
||||||
const status = response.status();
|
const status = response.status();
|
||||||
// Catch classic 404s and 500s on ANY fetch/image/script
|
// Catch classic 404s and 500s on ANY fetch/image/script
|
||||||
if (
|
if (
|
||||||
status >= 400 &&
|
status >= 400 &&
|
||||||
status !== 999 &&
|
status !== 999 &&
|
||||||
!response.url().includes('google-analytics') &&
|
!response.url().includes("google-analytics") &&
|
||||||
!response.url().includes('googletagmanager')
|
!response.url().includes("googletagmanager")
|
||||||
) {
|
) {
|
||||||
const type = response.request().resourceType();
|
const type = response.request().resourceType();
|
||||||
|
|
||||||
// We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s.
|
// 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({
|
brokenAssetsList.push({
|
||||||
url: response.url(),
|
url: response.url(),
|
||||||
status: status,
|
status: status,
|
||||||
@@ -145,7 +165,7 @@ async function main() {
|
|||||||
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
|
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
|
||||||
try {
|
try {
|
||||||
// Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger
|
// 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
|
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
@@ -178,7 +198,9 @@ async function main() {
|
|||||||
|
|
||||||
// 4. Report Results
|
// 4. Report Results
|
||||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
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);
|
console.table(brokenAssetsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +210,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasBrokenAssets || hasConsoleErrors) {
|
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);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -1,45 +1,49 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from "cheerio";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
const targetUrl =
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
process.argv[2] ||
|
||||||
|
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||||
|
"http://localhost:3000";
|
||||||
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||||
console.log(`📊 Limit: None (Full Sitemap)\n`);
|
console.log(`📊 Limit: None (Full Sitemap)\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||||
|
|
||||||
const response = await axios.get(sitemapUrl, {
|
const response = await axios.get(sitemapUrl, {
|
||||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||||
validateStatus: (status) => status < 400,
|
validateStatus: (status) => status < 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||||
let urls = $('url loc')
|
let urls = $("url loc")
|
||||||
.map((i, el) => $(el).text())
|
.map((i, el) => $(el).text())
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
const urlPattern = /https?:\/\/[^\/]+/;
|
const urlPattern = /https?:\/\/[^\/]+/;
|
||||||
urls = [...new Set(urls)]
|
urls = [...new Set(urls)]
|
||||||
.filter((u) => u.startsWith('http'))
|
.filter((u) => u.startsWith("http"))
|
||||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||||
|
|
||||||
if (urls.length === 0) {
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
if (fs.existsSync(outputDir))
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||||
@@ -47,14 +51,19 @@ async function main() {
|
|||||||
const u = urls[i];
|
const u = urls[i];
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(u, {
|
const res = await axios.get(u, {
|
||||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
headers: {
|
||||||
|
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||||
|
},
|
||||||
validateStatus: (status) => status < 400,
|
validateStatus: (status) => status < 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate a safe filename that retains URL information
|
// Generate a safe filename that retains URL information
|
||||||
const urlStr = new URL(u);
|
const urlStr = new URL(u);
|
||||||
const safePath = (urlStr.pathname + urlStr.search).replace(/[^a-zA-Z0-9]/g, '_');
|
const safePath = (urlStr.pathname + urlStr.search).replace(
|
||||||
const filename = `${safePath || 'index'}.html`;
|
/[^a-zA-Z0-9]/g,
|
||||||
|
"_",
|
||||||
|
);
|
||||||
|
const filename = `${safePath || "index"}.html`;
|
||||||
|
|
||||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -66,7 +75,9 @@ async function main() {
|
|||||||
|
|
||||||
console.log(`\n💻 Executing html-validate...`);
|
console.log(`\n💻 Executing html-validate...`);
|
||||||
try {
|
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!`);
|
console.log(`✅ HTML Validation passed perfectly!`);
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`❌ HTML Validation found issues.`);
|
console.error(`❌ HTML Validation found issues.`);
|
||||||
@@ -77,8 +88,9 @@ async function main() {
|
|||||||
console.error(`\n❌ Error during HTML Validation:`, errorBody);
|
console.error(`\n❌ Error during HTML Validation:`, errorBody);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
if (fs.existsSync(outputDir))
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from "cheerio";
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from "child_process";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
interface Pa11yConfig {
|
interface Pa11yConfig {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -36,9 +36,14 @@ interface Pa11yResult {
|
|||||||
* 3. Runs pa11y-ci on those URLs
|
* 3. Runs pa11y-ci on those URLs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
const targetUrl =
|
||||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
process.argv[2] ||
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
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() {
|
async function main() {
|
||||||
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
||||||
@@ -46,32 +51,32 @@ async function main() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Fetch Sitemap
|
// 1. Fetch Sitemap
|
||||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||||
|
|
||||||
const response = await axios.get(sitemapUrl, {
|
const response = await axios.get(sitemapUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||||
},
|
},
|
||||||
validateStatus: (status) => status < 400,
|
validateStatus: (status) => status < 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||||
let urls = $('url loc')
|
let urls = $("url loc")
|
||||||
.map((i, el) => $(el).text())
|
.map((i, el) => $(el).text())
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
// Cleanup, filter and normalize domains to targetUrl
|
// Cleanup, filter and normalize domains to targetUrl
|
||||||
const urlPattern = /https?:\/\/[^\/]+/;
|
const urlPattern = /https?:\/\/[^\/]+/;
|
||||||
urls = [...new Set(urls)]
|
urls = [...new Set(urls)]
|
||||||
.filter((u) => u.startsWith('http'))
|
.filter((u) => u.startsWith("http"))
|
||||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||||
|
|
||||||
if (urls.length === 0) {
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,9 @@ async function main() {
|
|||||||
console.log(
|
console.log(
|
||||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
`⚠️ 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));
|
const others = urls.filter((u) => !home.includes(u));
|
||||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||||
}
|
}
|
||||||
@@ -88,10 +95,10 @@ async function main() {
|
|||||||
urls.forEach((u) => console.log(` - ${u}`));
|
urls.forEach((u) => console.log(` - ${u}`));
|
||||||
|
|
||||||
// 2. Prepare pa11y-ci config
|
// 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: {} };
|
let baseConfig: Pa11yConfig = { defaults: {} };
|
||||||
if (fs.existsSync(baseConfigPath)) {
|
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)
|
// Extract domain for cookie (not currently used)
|
||||||
@@ -105,19 +112,21 @@ async function main() {
|
|||||||
defaults: {
|
defaults: {
|
||||||
...baseConfig.defaults,
|
...baseConfig.defaults,
|
||||||
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
||||||
runners: ['axe'],
|
runners: ["axe"],
|
||||||
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
|
ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"],
|
||||||
chromeLaunchConfig: {
|
chromeLaunchConfig: {
|
||||||
...baseConfig.defaults?.chromeLaunchConfig,
|
...baseConfig.defaults?.chromeLaunchConfig,
|
||||||
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
|
...(process.env.CHROME_PATH
|
||||||
|
? { executablePath: process.env.CHROME_PATH }
|
||||||
|
: {}),
|
||||||
args: [
|
args: [
|
||||||
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
||||||
'--no-sandbox',
|
"--no-sandbox",
|
||||||
'--disable-setuid-sandbox',
|
"--disable-setuid-sandbox",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||||
},
|
},
|
||||||
timeout: 60000, // Increase timeout for slower pages
|
timeout: 60000, // Increase timeout for slower pages
|
||||||
},
|
},
|
||||||
@@ -125,13 +134,13 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create output directory
|
// Create output directory
|
||||||
const outputDir = path.join(process.cwd(), '.pa11yci');
|
const outputDir = path.join(process.cwd(), ".pa11yci");
|
||||||
if (!fs.existsSync(outputDir)) {
|
if (!fs.existsSync(outputDir)) {
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempConfigPath = path.join(outputDir, 'config.temp.json');
|
const tempConfigPath = path.join(outputDir, "config.temp.json");
|
||||||
const reportPath = path.join(outputDir, 'report.json');
|
const reportPath = path.join(outputDir, "report.json");
|
||||||
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
||||||
|
|
||||||
// 3. Execute pa11y-ci
|
// 3. Execute pa11y-ci
|
||||||
@@ -140,8 +149,8 @@ async function main() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(pa11yCommand, {
|
execSync(pa11yCommand, {
|
||||||
encoding: 'utf8',
|
encoding: "utf8",
|
||||||
stdio: 'inherit',
|
stdio: "inherit",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// pa11y-ci exits with non-zero if issues are found, which is expected
|
// pa11y-ci exits with non-zero if issues are found, which is expected
|
||||||
@@ -149,7 +158,7 @@ async function main() {
|
|||||||
|
|
||||||
// 4. Summarize Results
|
// 4. Summarize Results
|
||||||
if (fs.existsSync(reportPath)) {
|
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`);
|
console.log(`\n📊 WCAG Audit Summary:\n`);
|
||||||
|
|
||||||
const summaryTable = Object.keys(reportData.results).map((url) => {
|
const summaryTable = Object.keys(reportData.results).map((url) => {
|
||||||
@@ -161,31 +170,47 @@ async function main() {
|
|||||||
|
|
||||||
if (Array.isArray(results)) {
|
if (Array.isArray(results)) {
|
||||||
// pa11y action execution errors come as objects with a message but no type
|
// pa11y action execution errors come as objects with a message but no type
|
||||||
const actionErrors = results.filter((r: Pa11yResult) => !r.type && r.message).length;
|
const actionErrors = results.filter(
|
||||||
errors = results.filter((r: Pa11yResult) => r.type === 'error').length + actionErrors;
|
(r: Pa11yResult) => !r.type && r.message,
|
||||||
warnings = results.filter((r: Pa11yResult) => r.type === 'warning').length;
|
).length;
|
||||||
notices = results.filter((r: Pa11yResult) => r.type === 'notice').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
|
// Clean URL for display
|
||||||
const displayUrl = url.replace(targetUrl, '') || '/';
|
const displayUrl = url.replace(targetUrl, "") || "/";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
|
URL:
|
||||||
|
displayUrl.length > 50
|
||||||
|
? displayUrl.substring(0, 47) + "..."
|
||||||
|
: displayUrl,
|
||||||
Errors: errors,
|
Errors: errors,
|
||||||
Warnings: warnings,
|
Warnings: warnings,
|
||||||
Notices: notices,
|
Notices: notices,
|
||||||
Status: errors === 0 ? '✅' : '❌',
|
Status: errors === 0 ? "✅" : "❌",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.table(summaryTable);
|
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 totalPages = summaryTable.length;
|
||||||
const cleanPages = summaryTable.filter((p) => p.Errors === 0).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) {
|
if (totalErrors > 0) {
|
||||||
console.log(` Total Errors discovered: ${totalErrors}`);
|
console.log(` Total Errors discovered: ${totalErrors}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
@@ -205,7 +230,10 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temp config file, keep report
|
// 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);
|
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
test_output.txt
Normal file
76
test_output.txt
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user