Compare commits
47
Commits
v1.3.20
...
f4aad36ea6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4aad36ea6 | ||
|
|
88edf08993 | ||
|
|
e0cdac971f | ||
|
|
cc728410c2 | ||
|
|
59e1ae7926 | ||
|
|
a36a7fe19d | ||
|
|
13cbc0291f | ||
|
|
1315985ba2 | ||
|
|
04309e3c02 | ||
|
|
8e2a6a0378 | ||
|
|
5a48dcd75f | ||
|
|
73cb55ee8d | ||
|
|
5ffd78d60f | ||
|
|
b0b8902f30 | ||
|
|
0dcb64ddca | ||
|
|
6bd4914586 | ||
|
|
4de69aa7ed | ||
|
|
e217d9c7f7 | ||
|
|
f97b37d3c8 | ||
|
|
d4705d616b | ||
|
|
d336980f27 | ||
|
|
f663497277 | ||
|
|
3dee7c73f1 | ||
|
|
ac4677ca50 | ||
|
|
ce492c221c | ||
|
|
fbd405c270 | ||
|
|
acfee13df7 | ||
|
|
a5d43df53a | ||
|
|
adda98eb00 | ||
|
|
3523547eca | ||
|
|
2ddef98924 | ||
|
|
272d3eccbf | ||
|
|
24475adef3 | ||
|
|
473079181f | ||
|
|
67e86b0fa3 | ||
|
|
1c441b2d99 | ||
|
|
e7446a5444 | ||
|
|
d50daa8db9 | ||
|
|
b69986572e | ||
|
|
18e02e3a33 | ||
|
|
f726b3e978 | ||
|
|
832be8c6bf | ||
|
|
2b934dd70e | ||
|
|
33e84fb538 | ||
|
|
52f4f30336 | ||
|
|
e8b8a4073e | ||
|
|
5821a5dee4 |
@@ -25,7 +25,9 @@ jobs:
|
|||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: |
|
||||||
|
rm -rf .next .turbo node_modules || true
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
- name: 🧪 Parallel Checks
|
- name: 🧪 Parallel Checks
|
||||||
run: |
|
run: |
|
||||||
pnpm lint &
|
pnpm lint &
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Infrastructure Maintenance: Production stabilization and email adapter fix triggered on 2026-04-12.
|
# Infrastructure Maintenance: Production stabilization and runner cleanup triggered on 2026-04-23.
|
||||||
name: 🚀 Build & Deploy
|
name: 🚀 Build & Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -140,6 +140,12 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: 🧹 Clean Runner Infrastructure
|
||||||
|
run: |
|
||||||
|
docker builder prune -f --filter "until=24h"
|
||||||
|
docker image prune -f --filter "until=24h"
|
||||||
|
docker system prune -f --volumes --filter "until=24h" || true
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -156,6 +162,7 @@ jobs:
|
|||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
rm -rf .next .turbo node_modules || true
|
||||||
pnpm store prune
|
pnpm store prune
|
||||||
pnpm install --no-frozen-lockfile
|
pnpm install --no-frozen-lockfile
|
||||||
- name: 🧪 QA Checks
|
- name: 🧪 QA Checks
|
||||||
@@ -185,6 +192,11 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: 🐳 Set up Docker Buildx
|
- name: 🐳 Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: 🧹 Pre-Build Cache Cleanup
|
||||||
|
run: |
|
||||||
|
docker builder prune -a -f --filter "until=12h"
|
||||||
|
docker image prune -a -f --filter "until=12h"
|
||||||
|
docker system prune -f --volumes || true
|
||||||
- name: 🔐 Registry Login
|
- name: 🔐 Registry Login
|
||||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
- name: 🏗️ Build and Push
|
- name: 🏗️ Build and Push
|
||||||
|
|||||||
+252
-11
@@ -1,17 +1,258 @@
|
|||||||
name: Nightly QA
|
name: Nightly QA (Inlined)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 4 * * *'
|
- cron: '0 2 * * *' # Every night at 2:00 AM
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/qa.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-qa-workflow:
|
prepare:
|
||||||
uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main
|
name: 🏗️ Prepare & Install
|
||||||
with:
|
runs-on: docker
|
||||||
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
container:
|
||||||
PROJECT_NAME: 'mb-grid-solutions'
|
image: catthehacker/ubuntu:act-latest
|
||||||
secrets:
|
steps:
|
||||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
- name: Checkout repository
|
||||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
uses: actions/checkout@v4
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
- 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: Install dependencies
|
||||||
|
run: |
|
||||||
|
rm -rf .next .turbo node_modules || true
|
||||||
|
pnpm install --frozen-lockfile --reporter=append-only
|
||||||
|
|
||||||
|
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: 🔐 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: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||||
|
- name: 🛠️ Fix @mintel/cli Syntax Error
|
||||||
|
run: |
|
||||||
|
CLI_PATH=$(find node_modules -name index.js | grep "@mintel/cli/dist/index.js" | head -n 1)
|
||||||
|
if [ -n "$CLI_PATH" ] && [ -f "$CLI_PATH" ]; then
|
||||||
|
echo "Patching CLI at $CLI_PATH"
|
||||||
|
sed -i '2{/^#!\/usr\/bin\/env node/d;}' "$CLI_PATH"
|
||||||
|
fi
|
||||||
|
- name: 🌐 HTML Validation
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com'
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||||
|
run: pnpm run check:html
|
||||||
|
- name: 🔍 Install Browser Libs
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||||
|
- name: 🕷️ Install Puppeteer Chrome
|
||||||
|
run: pnpm dlx puppeteer browsers install chrome
|
||||||
|
- name: 🖼️ Asset Scan
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com'
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||||
|
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: 🔐 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: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||||
|
- name: 🌐 Install Chromium
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y software-properties-common gnupg wget ca-certificates
|
||||||
|
sudo add-apt-repository -y ppa:xtradeb/apps
|
||||||
|
sudo apt-get update || true
|
||||||
|
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium
|
||||||
|
sudo ln -sf /usr/bin/chromium-browser /usr/bin/google-chrome || sudo 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 || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||||
|
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: 🔐 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: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||||
|
- name: 📦 Depcheck
|
||||||
|
run: |
|
||||||
|
pnpm dlx depcheck . --ignores="framer-motion,lucide-react,next-intl,vitest,@testing-library/jest-dom,@testing-library/react,@vitejs/plugin-react,autoprefixer,axios,cheerio,eslint-config-next,happy-dom,html-validate,husky,jsdom,lint-staged,pa11y-ci,pino-pretty,postcss,prettier,puppeteer,tailwindcss,tsx,typescript,@mintel/eslint-config,@mintel/husky-config,@mintel/tsconfig,@tailwindcss/postcss,@types/node,@types/nodemailer,@types/react,@types/react-dom"
|
||||||
|
- name: 🔗 Lychee Link Check
|
||||||
|
uses: lycheeverse/lychee-action@v2
|
||||||
|
with:
|
||||||
|
args: --accept 200,204,429 --timeout 15 app/ public/
|
||||||
|
fail: true
|
||||||
|
failIfEmpty: false
|
||||||
|
|
||||||
|
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: 🔐 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: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||||
|
- name: 🌐 Install Chromium
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y software-properties-common gnupg wget ca-certificates
|
||||||
|
sudo add-apt-repository -y ppa:xtradeb/apps
|
||||||
|
sudo apt-get update || true
|
||||||
|
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium
|
||||||
|
sudo ln -sf /usr/bin/chromium-browser /usr/bin/google-chrome || sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||||
|
- name: 🛠️ Fix @mintel/cli Syntax Error
|
||||||
|
run: |
|
||||||
|
CLI_PATH=$(find node_modules -name index.js | grep "@mintel/cli/dist/index.js" | head -n 1)
|
||||||
|
if [ -n "$CLI_PATH" ] && [ -f "$CLI_PATH" ]; then
|
||||||
|
echo "Patching CLI at $CLI_PATH"
|
||||||
|
sed -i '2{/^#!\/usr\/bin\/env node/d;}' "$CLI_PATH"
|
||||||
|
fi
|
||||||
|
- name: 🎭 LHCI Desktop
|
||||||
|
env:
|
||||||
|
LHCI_URL: 'https://testing.mb-grid-solutions.com'
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||||
|
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=desktop
|
||||||
|
- name: 📱 LHCI Mobile
|
||||||
|
env:
|
||||||
|
LHCI_URL: 'https://testing.mb-grid-solutions.com'
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||||
|
run: pnpm exec mintel 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
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,22 +13,27 @@ export default function AVB() {
|
|||||||
.filter((b) => b !== "");
|
.filter((b) => b !== "");
|
||||||
|
|
||||||
// Extract title and stand more robustly
|
// Extract title and stand more robustly
|
||||||
const title = rawBlocks.find(b => b.startsWith("# "))?.replace(/^#\s+/, "") || "Allgemeine Verkaufsbedingungen";
|
const title =
|
||||||
const stand = rawBlocks.find(b => b.toLowerCase().startsWith("stand:")) || "Stand: April 2026";
|
rawBlocks.find((b) => b.startsWith("# "))?.replace(/^#\s+/, "") ||
|
||||||
|
"Allgemeine Verkaufsbedingungen";
|
||||||
|
const stand =
|
||||||
|
rawBlocks.find((b) => b.toLowerCase().startsWith("stand:")) ||
|
||||||
|
"Stand: April 2026";
|
||||||
|
|
||||||
const sections: { title: string; content: string[] }[] = [];
|
const sections: { title: string; content: string[] }[] = [];
|
||||||
let currentSection: { title: string; content: string[] } | null = null;
|
let currentSection: { title: string; content: string[] } | null = null;
|
||||||
|
|
||||||
// Process sections, skipping title and stand blocks
|
// Process sections, skipping title and stand blocks
|
||||||
rawBlocks.forEach((block) => {
|
rawBlocks.forEach((block) => {
|
||||||
if (block.startsWith("# ") || block.toLowerCase().startsWith("stand:")) return;
|
if (block.startsWith("# ") || block.toLowerCase().startsWith("stand:"))
|
||||||
|
return;
|
||||||
|
|
||||||
if (block.startsWith("##")) {
|
if (block.startsWith("##")) {
|
||||||
// New section header: e.g. "## 1. Geltungsbereich"
|
// New section header: e.g. "## 1. Geltungsbereich"
|
||||||
if (currentSection) sections.push(currentSection);
|
if (currentSection) sections.push(currentSection);
|
||||||
currentSection = {
|
currentSection = {
|
||||||
title: block.replace(/^##\s+/, "").trim(),
|
title: block.replace(/^##\s+/, "").trim(),
|
||||||
content: []
|
content: [],
|
||||||
};
|
};
|
||||||
} else if (currentSection) {
|
} else if (currentSection) {
|
||||||
// Clean up bold markers for better display
|
// Clean up bold markers for better display
|
||||||
@@ -39,7 +44,7 @@ export default function AVB() {
|
|||||||
if (currentSection) sections.push(currentSection);
|
if (currentSection) sections.push(currentSection);
|
||||||
|
|
||||||
// The very last block might be a footer/schlussbestimmung if it's not in a section
|
// The very last block might be a footer/schlussbestimmung if it's not in a section
|
||||||
// In our MD, everything is in a section, so we just use the last block of the last section as potential footer if we want,
|
// In our MD, everything is in a section, so we just use the last block of the last section as potential footer if we want,
|
||||||
// but the current UI expects a separate 'footer' variable.
|
// but the current UI expects a separate 'footer' variable.
|
||||||
const footer = "MB Grid Solutions & Services";
|
const footer = "MB Grid Solutions & Services";
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@ export default function AVB() {
|
|||||||
<p className="text-slate-500 font-medium">{stand}</p>
|
<p className="text-slate-500 font-medium">{stand}</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="/assets/AVB MB Grid 4-2026.pdf"
|
href="/assets/avb-mb-grid-4-2026.pdf"
|
||||||
download
|
download
|
||||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -102,14 +102,15 @@ export default function About() {
|
|||||||
<Reveal key={i} delay={i * 0.1}>
|
<Reveal key={i} delay={i * 0.1}>
|
||||||
<div className="card-modern !p-6 hover:-translate-y-1 transition-[box-shadow,transform] duration-300 relative overflow-hidden tech-card-border">
|
<div className="card-modern !p-6 hover:-translate-y-1 transition-[box-shadow,transform] duration-300 relative overflow-hidden tech-card-border">
|
||||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||||
<h3 className="text-xl font-bold text-primary">
|
<h2 className="text-xl font-bold text-primary">
|
||||||
{person.name}
|
{person.name}
|
||||||
</h3>
|
</h2>
|
||||||
<a
|
<a
|
||||||
href={person.linkedin}
|
href={person.linkedin}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#0077b5] hover:scale-110 transition-transform"
|
className="text-[#0077b5] hover:scale-110 transition-transform"
|
||||||
|
aria-label={`LinkedIn Profile of ${person.name}`}
|
||||||
>
|
>
|
||||||
<Linkedin size={20} />
|
<Linkedin size={20} />
|
||||||
</a>
|
</a>
|
||||||
@@ -154,9 +155,9 @@ export default function About() {
|
|||||||
<div className="text-accent mb-6">
|
<div className="text-accent mb-6">
|
||||||
<Icon size={32} />
|
<Icon size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-xl font-bold text-white mb-4">
|
<h3 className="text-xl font-bold text-white mb-4">
|
||||||
{i + 1}. {item.title}
|
{i + 1}. {item.title}
|
||||||
</h4>
|
</h3>
|
||||||
<p className="text-slate-400 leading-relaxed">
|
<p className="text-slate-400 leading-relaxed">
|
||||||
{item.desc}
|
{item.desc}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ export default function Contact() {
|
|||||||
<Mail size={24} />
|
<Mail size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||||
{t("info.email")}
|
{t("info.email")}
|
||||||
</h4>
|
</h2>
|
||||||
<a
|
<a
|
||||||
href="mailto:info@mb-grid-solutions.com"
|
href="mailto:info@mb-grid-solutions.com"
|
||||||
className="text-white text-lg md:text-xl font-bold hover:text-accent transition-colors break-all"
|
className="text-white text-lg md:text-xl font-bold hover:text-accent transition-colors break-all"
|
||||||
@@ -144,9 +144,9 @@ export default function Contact() {
|
|||||||
<MapPin size={24} />
|
<MapPin size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||||
{t("info.address")}
|
{t("info.address")}
|
||||||
</h4>
|
</h2>
|
||||||
<p className="text-white text-lg md:text-xl font-bold leading-relaxed">
|
<p className="text-white text-lg md:text-xl font-bold leading-relaxed">
|
||||||
{t("info.company")}
|
{t("info.company")}
|
||||||
<br />
|
<br />
|
||||||
@@ -162,12 +162,8 @@ export default function Contact() {
|
|||||||
<div className="w-full h-[300px] rounded-[2.5rem] overflow-hidden border border-white/10 shadow-sm grayscale hover:grayscale-0 transition-all duration-700 relative group">
|
<div className="w-full h-[300px] rounded-[2.5rem] overflow-hidden border border-white/10 shadow-sm grayscale hover:grayscale-0 transition-all duration-700 relative group">
|
||||||
<div className="absolute inset-0 border-2 border-accent/0 group-hover:border-accent/20 transition-all duration-500 z-10 pointer-events-none rounded-[2.5rem]" />
|
<div className="absolute inset-0 border-2 border-accent/0 group-hover:border-accent/20 transition-all duration-500 z-10 pointer-events-none rounded-[2.5rem]" />
|
||||||
<iframe
|
<iframe
|
||||||
width="100%"
|
className="w-full h-full border-0"
|
||||||
height="100%"
|
title={t("info.mapTitle") || "Location Map"}
|
||||||
frameBorder="0"
|
|
||||||
scrolling="no"
|
|
||||||
marginHeight={0}
|
|
||||||
marginWidth={0}
|
|
||||||
src="https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552"
|
src="https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,9 +180,9 @@ export default function Contact() {
|
|||||||
<div className="w-20 h-20 rounded-full bg-accent/10 text-accent flex items-center justify-center mx-auto mb-8">
|
<div className="w-20 h-20 rounded-full bg-accent/10 text-accent flex items-center justify-center mx-auto mb-8">
|
||||||
<CheckCircle size={40} />
|
<CheckCircle size={40} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-3xl font-bold text-primary mb-4">
|
<h2 className="text-3xl font-bold text-primary mb-4">
|
||||||
{t("form.successTitle")}
|
{t("form.successTitle")}
|
||||||
</h3>
|
</h2>
|
||||||
<p className="text-slate-600 text-lg mb-10">
|
<p className="text-slate-600 text-lg mb-10">
|
||||||
{t("form.successMessage")}
|
{t("form.successMessage")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -184,7 +184,11 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
<div className="container-custom relative z-10">
|
<div className="container-custom relative z-10">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<Link href="/" className="inline-block mb-6 md:mb-8 group">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block mb-6 md:mb-8 group"
|
||||||
|
aria-label={t("nav.home")}
|
||||||
|
>
|
||||||
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
||||||
<Image
|
<Image
|
||||||
src="/assets/logo.png"
|
src="/assets/logo.png"
|
||||||
@@ -203,9 +207,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-white font-bold mb-6">
|
<h2 className="text-white font-bold mb-6 text-base">
|
||||||
{t("footer.navigation")}
|
{t("footer.navigation")}
|
||||||
</h4>
|
</h2>
|
||||||
<nav className="flex flex-col gap-4">
|
<nav className="flex flex-col gap-4">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -226,9 +230,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-white font-bold mb-6">
|
<h2 className="text-white font-bold mb-6 text-base">
|
||||||
{t("footer.legal")}
|
{t("footer.legal")}
|
||||||
</h4>
|
</h2>
|
||||||
<nav className="flex flex-col gap-4">
|
<nav className="flex flex-col gap-4">
|
||||||
<Link
|
<Link
|
||||||
href="/impressum"
|
href="/impressum"
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ export const StatusModal = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
<h2 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h2>
|
||||||
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export const TechnicalSpecsSection = () => {
|
|||||||
<Reveal key={i} delay={i * 0.1}>
|
<Reveal key={i} delay={i * 0.1}>
|
||||||
<div className="p-10 rounded-3xl bg-white/5 border border-white/10 backdrop-blur-sm hover:bg-white/10 transition-colors h-full relative group overflow-hidden">
|
<div className="p-10 rounded-3xl bg-white/5 border border-white/10 backdrop-blur-sm hover:bg-white/10 transition-colors h-full relative group overflow-hidden">
|
||||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||||
<h4 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
<h3 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
||||||
{item.label}
|
{item.label}
|
||||||
</h4>
|
</h3>
|
||||||
<p className="text-2xl font-bold text-white mb-4 leading-tight">
|
<p className="text-2xl font-bold text-white mb-4 leading-tight">
|
||||||
{item.value}
|
{item.value}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+1
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mb-grid-solutions.com",
|
"name": "mb-grid-solutions.com",
|
||||||
"version": "1.3.20",
|
"version": "1.3.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -37,9 +37,7 @@
|
|||||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||||
"@payloadcms/storage-s3": "^3.77.0",
|
"@payloadcms/storage-s3": "^3.77.0",
|
||||||
"@payloadcms/ui": "^3.77.0",
|
"@payloadcms/ui": "^3.77.0",
|
||||||
"@react-email/components": "^1.0.8",
|
|
||||||
"@sentry/nextjs": "^10.38.0",
|
"@sentry/nextjs": "^10.38.0",
|
||||||
"bcryptjs": "^3.0.3",
|
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"graphql": "^16.13.0",
|
"graphql": "^16.13.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
@@ -50,7 +48,6 @@
|
|||||||
"pino": "^10.3.0",
|
"pino": "^10.3.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-email": "^5.2.8",
|
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+10
-772
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||||
|
|||||||
+33
-21
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Replace Registry Auth
|
||||||
|
old_auth = """ - 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"""
|
||||||
|
|
||||||
|
new_auth = """ - name: 🔐 Registry Auth
|
||||||
|
run: |
|
||||||
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
|
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||||
|
|
||||||
|
content = content.replace(old_auth, new_auth)
|
||||||
|
|
||||||
|
# Replace Install dependencies (for jobs that don't have the rm -rf)
|
||||||
|
old_install_1 = """ - name: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile"""
|
||||||
|
|
||||||
|
new_install_1 = """ - name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile"""
|
||||||
|
|
||||||
|
content = content.replace(old_install_1, new_install_1)
|
||||||
|
|
||||||
|
# Replace Install dependencies (for prepare job)
|
||||||
|
old_install_2 = """ - name: Install dependencies
|
||||||
|
run: |
|
||||||
|
rm -rf .next .turbo node_modules || true
|
||||||
|
pnpm install --no-frozen-lockfile --reporter=append-only"""
|
||||||
|
|
||||||
|
new_install_2 = """ - name: Install dependencies
|
||||||
|
run: |
|
||||||
|
rm -rf .next .turbo node_modules || true
|
||||||
|
pnpm install --frozen-lockfile --reporter=append-only"""
|
||||||
|
|
||||||
|
content = content.replace(old_install_2, new_install_2)
|
||||||
|
|
||||||
|
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
bad_auth = """ - name: 🔐 Registry Auth
|
||||||
|
run: |
|
||||||
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
|
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||||
|
|
||||||
|
good_auth = """ - 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"""
|
||||||
|
|
||||||
|
content = content.replace(bad_auth, good_auth)
|
||||||
|
|
||||||
|
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
+68
-40
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
+6
-5
@@ -1,5 +1,6 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
@@ -16,11 +17,11 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./tests/setup.tsx'],
|
setupFiles: ['./tests/setup.tsx'],
|
||||||
alias: {
|
alias: [
|
||||||
'next/server': 'next/server.js',
|
{ find: 'next/server', replacement: 'next/server.js' },
|
||||||
'@payload-config': new URL('./tests/__mocks__/payload-config.ts', import.meta.url).pathname,
|
{ find: '@payload-config', replacement: fileURLToPath(new URL('./tests/__mocks__/payload-config.ts', import.meta.url)) },
|
||||||
'@': new URL('./', import.meta.url).pathname,
|
{ find: '@', replacement: fileURLToPath(new URL('./', import.meta.url)) },
|
||||||
},
|
],
|
||||||
exclude: ['**/node_modules/**', '**/.next/**'],
|
exclude: ['**/node_modules/**', '**/.next/**'],
|
||||||
server: {
|
server: {
|
||||||
deps: {
|
deps: {
|
||||||
|
|||||||
Reference in New Issue
Block a user