Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dd5c077c5 | ||
|
|
dbae239031 | ||
|
|
bbcf859d12 | ||
|
|
81c6e02faf | ||
|
|
ed98c40079 | ||
|
|
0633326c1e | ||
|
|
cad27d6a02 | ||
|
|
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 | ||
|
|
dfc7536268 | ||
|
|
9f26a179fa | ||
|
|
5043058660 | ||
|
|
a37091ad71 | ||
|
|
9539aa35eb | ||
|
|
3ad24ef615 | ||
|
|
3018ae5412 | ||
|
|
fd3f4c82c5 | ||
|
|
3906838dc1 | ||
|
|
785e36af08 | ||
|
|
d4c32476d6 | ||
|
|
9b9d4634dd | ||
|
|
c9e48cd5a4 |
@@ -22,10 +22,13 @@ jobs:
|
||||
version: 10
|
||||
- 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
|
||||
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 || secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
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
|
||||
|
||||
on:
|
||||
@@ -140,6 +140,12 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -156,6 +162,7 @@ jobs:
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
@@ -185,6 +192,11 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
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
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
- name: 🏗️ Build and Push
|
||||
|
||||
+100
-11
@@ -1,17 +1,106 @@
|
||||
name: Nightly QA
|
||||
name: Nightly QA (Inlined)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [fix/nightly-qa-stabilization]
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
- cron: '0 2 * * *' # Every night at 02:00
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call-qa-workflow:
|
||||
uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main
|
||||
with:
|
||||
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
||||
PROJECT_NAME: 'mb-grid-solutions'
|
||||
secrets:
|
||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
||||
qa:
|
||||
name: 🧪 Quality Assurance
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: https://testing.mb-grid-solutions.com
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
steps:
|
||||
- name: 🛑 Cleanup Workspace
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
|
||||
- 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 || secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
|
||||
- name: 🏗️ Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🛠️ Patch @mintel/cli
|
||||
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: 🔍 Static Analysis
|
||||
id: static
|
||||
run: |
|
||||
echo "Running Lint..."
|
||||
pnpm run lint
|
||||
echo "Running Depcheck..."
|
||||
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,@payloadcms/ui,@commitlint/*"
|
||||
|
||||
- name: ♿ Accessibility Audit
|
||||
id: a11y
|
||||
continue-on-error: true
|
||||
run: pnpm run check:wcag
|
||||
|
||||
- name: 🔗 Link Checker
|
||||
id: links
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -sLO https://github.com/lycheeverse/lychee/releases/download/v0.14.3/lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
|
||||
tar -xzf lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
|
||||
mv lychee /usr/local/bin/
|
||||
lychee --accept 200,429,999 --exclude-mail --exclude "https://www.linkedin.com/.*" --exclude "https://fonts.gstatic.com/.*" https://testing.mb-grid-solutions.com
|
||||
|
||||
- name: 🎭 Performance & Lighthouse
|
||||
id: perf
|
||||
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=desktop
|
||||
|
||||
- name: 🔔 Notify Status
|
||||
if: always()
|
||||
run: |
|
||||
STATUS="success"
|
||||
if [[ "${{ steps.static.outcome }}" != "success" || "${{ steps.perf.outcome }}" != "success" ]]; then
|
||||
STATUS="failure"
|
||||
fi
|
||||
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"status": "$STATUS",
|
||||
"project": "mb-grid-solutions.com",
|
||||
"workflow": "Nightly QA",
|
||||
"details": {
|
||||
"Static": "${{ steps.static.outcome }}",
|
||||
"A11y": "${{ steps.a11y.outcome }}",
|
||||
"Links": "${{ steps.links.outcome }}",
|
||||
"Perf": "${{ steps.perf.outcome }}"
|
||||
},
|
||||
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
${{ secrets.WEBHOOK_URL }} || echo "Notification failed"
|
||||
|
||||
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
+31
-44
@@ -2,65 +2,51 @@ import { Download } from "lucide-react";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default function AGB() {
|
||||
export default function AVB() {
|
||||
const filePath = path.join(process.cwd(), "context/avbs.md");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs)
|
||||
const blocks = fileContent
|
||||
// Split by double newlines to get major blocks
|
||||
const rawBlocks = fileContent
|
||||
.split(/\n\s*\n/)
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b !== "");
|
||||
|
||||
const title = blocks[0] || "Liefer- und Zahlungsbedingungen";
|
||||
const stand = blocks[1] || "Stand Januar 2026";
|
||||
// Extract title and stand more robustly
|
||||
const title =
|
||||
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[] }[] = [];
|
||||
let currentSection: { title: string; content: string[] } | null = null;
|
||||
|
||||
// Skip title and stand
|
||||
blocks.slice(2).forEach((block) => {
|
||||
const lines = block
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l !== "");
|
||||
if (lines.length === 0) return;
|
||||
// Process sections, skipping title and stand blocks
|
||||
rawBlocks.forEach((block) => {
|
||||
if (block.startsWith("# ") || block.toLowerCase().startsWith("stand:"))
|
||||
return;
|
||||
|
||||
const firstLine = lines[0];
|
||||
|
||||
if (/^\d+\./.test(firstLine)) {
|
||||
// New section
|
||||
if (block.startsWith("##")) {
|
||||
// New section header: e.g. "## 1. Geltungsbereich"
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
currentSection = { title: firstLine, content: [] };
|
||||
|
||||
// If there are more lines in this block, they form the first paragraph(s)
|
||||
if (lines.length > 1) {
|
||||
// Join subsequent lines as they might be part of the same paragraph
|
||||
// In this MD, we'll assume lines in the same block belong together
|
||||
// unless they are clearly separate paragraphs (but we already split by double newline)
|
||||
const remainingText = lines.slice(1).join(" ");
|
||||
if (remainingText) currentSection.content.push(remainingText);
|
||||
}
|
||||
currentSection = {
|
||||
title: block.replace(/^##\s+/, "").trim(),
|
||||
content: [],
|
||||
};
|
||||
} else if (currentSection) {
|
||||
// Continuation of current section
|
||||
const blockText = lines.join(" ");
|
||||
if (blockText) currentSection.content.push(blockText);
|
||||
// Clean up bold markers for better display
|
||||
const cleanedBlock = block.replace(/\*\*(.*?)\*\*/g, "$1");
|
||||
currentSection.content.push(cleanedBlock);
|
||||
}
|
||||
});
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
// The last block is the footer
|
||||
const footer = blocks[blocks.length - 1];
|
||||
if (sections.length > 0) {
|
||||
const lastSection = sections[sections.length - 1];
|
||||
if (lastSection.content.includes(footer) || lastSection.title === footer) {
|
||||
lastSection.content = lastSection.content.filter((c) => c !== footer);
|
||||
if (sections[sections.length - 1].title === footer) {
|
||||
sections.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
// but the current UI expects a separate 'footer' variable.
|
||||
const footer = "MB Grid Solutions & Services";
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
|
||||
@@ -74,7 +60,7 @@ export default function AGB() {
|
||||
<p className="text-slate-500 font-medium">{stand}</p>
|
||||
</div>
|
||||
<a
|
||||
href="/assets/AVB MB Grid 4-2026.pdf"
|
||||
href="/assets/avb-mb-grid-4-2026.pdf"
|
||||
download
|
||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||
>
|
||||
@@ -114,8 +100,9 @@ export default function AGB() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-8 border-t border-slate-100">
|
||||
<p className="font-bold text-primary">{footer}</p>
|
||||
<div className="pt-8 border-t border-slate-100 flex justify-between items-center text-slate-400 text-sm italic">
|
||||
<p>{footer}</p>
|
||||
<p>{stand}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,14 +102,15 @@ export default function About() {
|
||||
<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="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}
|
||||
</h3>
|
||||
</h2>
|
||||
<a
|
||||
href={person.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#0077b5] hover:scale-110 transition-transform"
|
||||
aria-label={`LinkedIn Profile of ${person.name}`}
|
||||
>
|
||||
<Linkedin size={20} />
|
||||
</a>
|
||||
@@ -154,9 +155,9 @@ export default function About() {
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
</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}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-slate-400 leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
|
||||
@@ -125,9 +125,9 @@ export default function Contact() {
|
||||
<Mail size={24} />
|
||||
</div>
|
||||
<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")}
|
||||
</h4>
|
||||
</h2>
|
||||
<a
|
||||
href="mailto:info@mb-grid-solutions.com"
|
||||
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} />
|
||||
</div>
|
||||
<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")}
|
||||
</h4>
|
||||
</h2>
|
||||
<p className="text-white text-lg md:text-xl font-bold leading-relaxed">
|
||||
{t("info.company")}
|
||||
<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="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
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
marginHeight={0}
|
||||
marginWidth={0}
|
||||
className="w-full h-full border-0"
|
||||
title={t("info.mapTitle") || "Location Map"}
|
||||
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>
|
||||
</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">
|
||||
<CheckCircle size={40} />
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-primary mb-4">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">
|
||||
{t("form.successTitle")}
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10">
|
||||
{t("form.successMessage")}
|
||||
</p>
|
||||
|
||||
+10
-6
@@ -184,7 +184,11 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<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="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">
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
@@ -203,9 +207,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.navigation")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
@@ -226,9 +230,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.legal")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/impressum"
|
||||
@@ -246,7 +250,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
href="/agb"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("footer.agb")}
|
||||
{t("footer.avb")}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -75,9 +75,9 @@ export const StatusModal = ({
|
||||
)}
|
||||
</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}
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
@@ -59,9 +59,9 @@ export const TechnicalSpecsSection = () => {
|
||||
<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="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}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-white mb-4 leading-tight">
|
||||
{item.value}
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@
|
||||
"legal": "Rechtliches",
|
||||
"impressum": "Impressum",
|
||||
"datenschutz": "Datenschutz",
|
||||
"agb": "AVB",
|
||||
"avb": "AVB",
|
||||
"rights": "Alle Rechte vorbehalten.",
|
||||
"madeWith": "Entwickelt mit",
|
||||
"precision": "Präzision",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
+24
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mb-grid-solutions.com",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.22",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"scripts": {
|
||||
@@ -29,17 +29,15 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.999.0",
|
||||
"@mintel/mail": "^1.8.21",
|
||||
"@mintel/next-config": "^1.8.20",
|
||||
"@mintel/next-utils": "^1.8.20",
|
||||
"@mintel/next-config": "^1.8.21",
|
||||
"@mintel/next-utils": "^1.8.21",
|
||||
"@payloadcms/db-postgres": "^3.77.0",
|
||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||
"@payloadcms/next": "^3.77.0",
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/storage-s3": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-email/components": "^1.0.8",
|
||||
"@sentry/nextjs": "^10.38.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.13.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -50,17 +48,16 @@
|
||||
"pino": "^10.3.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-email": "^5.2.8",
|
||||
"sharp": "^0.34.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.4.0",
|
||||
"@commitlint/config-conventional": "^20.4.0",
|
||||
"@mintel/cli": "^1.8.20",
|
||||
"@mintel/eslint-config": "^1.8.20",
|
||||
"@mintel/husky-config": "^1.8.20",
|
||||
"@mintel/tsconfig": "^1.8.20",
|
||||
"@mintel/cli": "^1.8.21",
|
||||
"@mintel/eslint-config": "^1.8.21",
|
||||
"@mintel/husky-config": "^1.8.21",
|
||||
"@mintel/tsconfig": "^1.8.21",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -69,26 +66,37 @@
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"axios": "^1.13.5",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"axios": "^1.13.6",
|
||||
"cheerio": "^1.2.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"happy-dom": "^20.6.1",
|
||||
"html-validate": "^10.8.0",
|
||||
"html-validate": "^10.13.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"pa11y-ci": "^4.0.1",
|
||||
"pa11y-ci": "^4.1.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.5.0",
|
||||
"puppeteer": "^24.37.3",
|
||||
"prettier": "^3.8.1",
|
||||
"puppeteer": "^24.42.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"puppeteer",
|
||||
"sharp",
|
||||
"esbuild",
|
||||
"@parcel/watcher",
|
||||
"@swc/core",
|
||||
"@sentry/cli",
|
||||
"unrs-resolver"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.infra.mintel.me:2222/mmintel/mb-grid-solutions.com.git"
|
||||
|
||||
Generated
+779
-770
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,40 @@
|
||||
import puppeteer, { HTTPResponse } from 'puppeteer';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import puppeteer, { HTTPResponse } from "puppeteer";
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const targetUrl =
|
||||
process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) ||
|
||||
process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
'http://localhost:3000';
|
||||
const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.ASSET_CHECK_LIMIT
|
||||
? parseInt(process.env.ASSET_CHECK_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: ${limit} pages\n`);
|
||||
|
||||
// 1. Fetch Sitemap to discover all routes
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
let urls: string[] = [];
|
||||
|
||||
try {
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
urls = $('url loc')
|
||||
urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Normalize to target URL instance
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
@@ -42,13 +44,20 @@ async function main() {
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
// Simplify selection: home pages + a slice of the rest
|
||||
const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith('/en'));
|
||||
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
||||
urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
||||
const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith("/en"));
|
||||
const others = urls.filter(
|
||||
(u) => !homeEN.includes(u) && !homeDE.includes(u),
|
||||
);
|
||||
urls = [
|
||||
...homeDE,
|
||||
...homeEN,
|
||||
...others.slice(0, limit - (homeEN.length + homeDE.length)),
|
||||
];
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`❌ Failed to fetch sitemap: ${errorBody}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -56,50 +65,64 @@ async function main() {
|
||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
process.env.CHROME_PATH ||
|
||||
undefined,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Inject Gatekeeper session bypassing auth screens
|
||||
await page.setCookie({
|
||||
name: 'mb_gatekeeper_session',
|
||||
name: "mintel_gatekeeper_session",
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: targetUrl.startsWith('https://'),
|
||||
secure: targetUrl.startsWith("https://"),
|
||||
});
|
||||
|
||||
let hasBrokenAssets = false;
|
||||
let hasConsoleErrors = false;
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> = [];
|
||||
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> =
|
||||
[];
|
||||
const consoleErrorsList: Array<{
|
||||
type: string;
|
||||
error: string;
|
||||
page: string;
|
||||
}> = [];
|
||||
|
||||
// Listen for unhandled exceptions natively in the page
|
||||
page.on('pageerror', (err: any) => {
|
||||
page.on("pageerror", (err: unknown) => {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
error: err.message,
|
||||
type: "PAGE_ERROR",
|
||||
error: errorMessage,
|
||||
page: page.url(),
|
||||
});
|
||||
hasConsoleErrors = true;
|
||||
});
|
||||
|
||||
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
|
||||
page.on('console', (msg) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
page.on("console", (msg: any) => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warn') {
|
||||
if (type === "error" || type === "warn") {
|
||||
const text = msg.text();
|
||||
|
||||
// Exclude common browser extension noise or third party tracker warnings
|
||||
if (
|
||||
text.includes('google-analytics') ||
|
||||
text.includes('googletagmanager') ||
|
||||
text.includes('SES Removing unpermitted intrinsics') ||
|
||||
text.includes('Third-party cookie will be blocked') ||
|
||||
text.includes('Fast Refresh')
|
||||
text.includes("google-analytics") ||
|
||||
text.includes("googletagmanager") ||
|
||||
text.includes("SES Removing unpermitted intrinsics") ||
|
||||
text.includes("Third-party cookie will be blocked") ||
|
||||
text.includes("Fast Refresh")
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -113,19 +136,19 @@ async function main() {
|
||||
});
|
||||
|
||||
// Listen to ALL network responses
|
||||
page.on('response', (response: HTTPResponse) => {
|
||||
page.on("response", (response: HTTPResponse) => {
|
||||
const status = response.status();
|
||||
// Catch classic 404s and 500s on ANY fetch/image/script
|
||||
if (
|
||||
status >= 400 &&
|
||||
status !== 999 &&
|
||||
!response.url().includes('google-analytics') &&
|
||||
!response.url().includes('googletagmanager')
|
||||
!response.url().includes("google-analytics") &&
|
||||
!response.url().includes("googletagmanager")
|
||||
) {
|
||||
const type = response.request().resourceType();
|
||||
|
||||
// We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s.
|
||||
if (['image', 'script', 'stylesheet', 'fetch', 'xhr'].includes(type)) {
|
||||
if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) {
|
||||
brokenAssetsList.push({
|
||||
url: response.url(),
|
||||
status: status,
|
||||
@@ -142,7 +165,7 @@ async function main() {
|
||||
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
|
||||
try {
|
||||
// Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger
|
||||
await page.goto(u, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 });
|
||||
|
||||
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||
await page.evaluate(async () => {
|
||||
@@ -163,8 +186,9 @@ async function main() {
|
||||
|
||||
// Wait a tiny bit more for final lazy loads
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
} catch (err: any) {
|
||||
console.error(`⚠️ Timeout or navigation error on ${u}: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`⚠️ Timeout or navigation error on ${u}: ${errorBody}`);
|
||||
// Don't fail the whole script just because one page timed out, but flag it
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
@@ -174,7 +198,9 @@ async function main() {
|
||||
|
||||
// 4. Report Results
|
||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
|
||||
console.error(
|
||||
`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`,
|
||||
);
|
||||
console.table(brokenAssetsList);
|
||||
}
|
||||
|
||||
@@ -184,7 +210,9 @@ async function main() {
|
||||
}
|
||||
|
||||
if (hasBrokenAssets || hasConsoleErrors) {
|
||||
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
|
||||
console.error(
|
||||
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
|
||||
+41
-27
@@ -1,45 +1,49 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: None (Full Sitemap)\n`);
|
||||
|
||||
try {
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||
@@ -47,36 +51,46 @@ async function main() {
|
||||
const u = urls[i];
|
||||
try {
|
||||
const res = await axios.get(u, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: {
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
// Generate a safe filename that retains URL information
|
||||
const urlStr = new URL(u);
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const filename = `${safePath || 'index'}.html`;
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
"_",
|
||||
);
|
||||
const filename = `${safePath || "index"}.html`;
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||
} catch (err: any) {
|
||||
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
|
||||
throw new Error(`Failed to fetch page: ${u} - ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`❌ HTTP Error fetching ${u}: ${errorBody}`);
|
||||
throw new Error(`Failed to fetch page: ${u} - ${errorBody}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n💻 Executing html-validate...`);
|
||||
try {
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`✅ HTML Validation passed perfectly!`);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error(`❌ HTML Validation found issues.`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`\n❌ Error during HTML Validation:`, error.message);
|
||||
} catch (error: unknown) {
|
||||
const errorBody = error instanceof Error ? error.message : String(error);
|
||||
console.error(`\n❌ Error during HTML Validation:`, errorBody);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
+101
-47
@@ -1,8 +1,32 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
interface Pa11yConfig {
|
||||
defaults: {
|
||||
threshold?: number;
|
||||
runners?: string[];
|
||||
ignore?: string[];
|
||||
chromeLaunchConfig?: {
|
||||
executablePath?: string;
|
||||
args?: string[];
|
||||
};
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
};
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
interface Pa11yResult {
|
||||
type?: string;
|
||||
message?: string;
|
||||
code?: string;
|
||||
context?: string;
|
||||
selector?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WCAG Audit Script
|
||||
@@ -12,9 +36,14 @@ import * as path from 'path';
|
||||
* 3. Runs pa11y-ci on those URLs
|
||||
*/
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.PAGESPEED_LIMIT
|
||||
? parseInt(process.env.PAGESPEED_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
||||
@@ -22,32 +51,32 @@ async function main() {
|
||||
|
||||
try {
|
||||
// 1. Fetch Sitemap
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Cleanup, filter and normalize domains to targetUrl
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -55,7 +84,9 @@ async function main() {
|
||||
console.log(
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl);
|
||||
const home = urls.filter(
|
||||
(u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl,
|
||||
);
|
||||
const others = urls.filter((u) => !home.includes(u));
|
||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||
}
|
||||
@@ -64,15 +95,16 @@ async function main() {
|
||||
urls.forEach((u) => console.log(` - ${u}`));
|
||||
|
||||
// 2. Prepare pa11y-ci config
|
||||
const baseConfigPath = path.join(process.cwd(), '.pa11yci.json');
|
||||
let baseConfig: any = { defaults: {} };
|
||||
const baseConfigPath = path.join(process.cwd(), ".pa11yci.json");
|
||||
let baseConfig: Pa11yConfig = { defaults: {} };
|
||||
if (fs.existsSync(baseConfigPath)) {
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
|
||||
}
|
||||
|
||||
// Extract domain for cookie
|
||||
const urlObj = new URL(targetUrl);
|
||||
const domain = urlObj.hostname;
|
||||
// Extract domain for cookie (not currently used)
|
||||
// const urlObj = new URL(targetUrl);
|
||||
// domain is not used, so remove or keep if needed later? Linter says unused.
|
||||
// const domain = urlObj.hostname;
|
||||
|
||||
// Update config with discovered URLs and gatekeeper cookie
|
||||
const tempConfig = {
|
||||
@@ -80,19 +112,21 @@ async function main() {
|
||||
defaults: {
|
||||
...baseConfig.defaults,
|
||||
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
||||
runners: ['axe'],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
|
||||
runners: ["axe"],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"],
|
||||
chromeLaunchConfig: {
|
||||
...baseConfig.defaults?.chromeLaunchConfig,
|
||||
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
|
||||
...(process.env.CHROME_PATH
|
||||
? { executablePath: process.env.CHROME_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
timeout: 60000, // Increase timeout for slower pages
|
||||
},
|
||||
@@ -100,13 +134,13 @@ async function main() {
|
||||
};
|
||||
|
||||
// Create output directory
|
||||
const outputDir = path.join(process.cwd(), '.pa11yci');
|
||||
const outputDir = path.join(process.cwd(), ".pa11yci");
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempConfigPath = path.join(outputDir, 'config.temp.json');
|
||||
const reportPath = path.join(outputDir, 'report.json');
|
||||
const tempConfigPath = path.join(outputDir, "config.temp.json");
|
||||
const reportPath = path.join(outputDir, "report.json");
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
||||
|
||||
// 3. Execute pa11y-ci
|
||||
@@ -115,16 +149,16 @@ async function main() {
|
||||
|
||||
try {
|
||||
execSync(pa11yCommand, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// pa11y-ci exits with non-zero if issues are found, which is expected
|
||||
}
|
||||
|
||||
// 4. Summarize Results
|
||||
if (fs.existsSync(reportPath)) {
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||||
console.log(`\n📊 WCAG Audit Summary:\n`);
|
||||
|
||||
const summaryTable = Object.keys(reportData.results).map((url) => {
|
||||
@@ -136,31 +170,47 @@ async function main() {
|
||||
|
||||
if (Array.isArray(results)) {
|
||||
// pa11y action execution errors come as objects with a message but no type
|
||||
const actionErrors = results.filter((r: any) => !r.type && r.message).length;
|
||||
errors = results.filter((r: any) => r.type === 'error').length + actionErrors;
|
||||
warnings = results.filter((r: any) => r.type === 'warning').length;
|
||||
notices = results.filter((r: any) => r.type === 'notice').length;
|
||||
const actionErrors = results.filter(
|
||||
(r: Pa11yResult) => !r.type && r.message,
|
||||
).length;
|
||||
errors =
|
||||
results.filter((r: Pa11yResult) => r.type === "error").length +
|
||||
actionErrors;
|
||||
warnings = results.filter(
|
||||
(r: Pa11yResult) => r.type === "warning",
|
||||
).length;
|
||||
notices = results.filter(
|
||||
(r: Pa11yResult) => r.type === "notice",
|
||||
).length;
|
||||
}
|
||||
|
||||
// Clean URL for display
|
||||
const displayUrl = url.replace(targetUrl, '') || '/';
|
||||
const displayUrl = url.replace(targetUrl, "") || "/";
|
||||
|
||||
return {
|
||||
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
|
||||
URL:
|
||||
displayUrl.length > 50
|
||||
? displayUrl.substring(0, 47) + "..."
|
||||
: displayUrl,
|
||||
Errors: errors,
|
||||
Warnings: warnings,
|
||||
Notices: notices,
|
||||
Status: errors === 0 ? '✅' : '❌',
|
||||
Status: errors === 0 ? "✅" : "❌",
|
||||
};
|
||||
});
|
||||
|
||||
console.table(summaryTable);
|
||||
|
||||
const totalErrors = summaryTable.reduce((acc, curr) => acc + curr.Errors, 0);
|
||||
const totalErrors = summaryTable.reduce(
|
||||
(acc, curr) => acc + curr.Errors,
|
||||
0,
|
||||
);
|
||||
const totalPages = summaryTable.length;
|
||||
const cleanPages = summaryTable.filter((p) => p.Errors === 0).length;
|
||||
|
||||
console.log(`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`);
|
||||
console.log(
|
||||
`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`,
|
||||
);
|
||||
if (totalErrors > 0) {
|
||||
console.log(` Total Errors discovered: ${totalErrors}`);
|
||||
process.exitCode = 1;
|
||||
@@ -168,18 +218,22 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log(`\n✨ WCAG Audit completed!`);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`\n❌ Error during WCAG Audit:`);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error instanceof AxiosError) {
|
||||
console.error(`Status: ${error.response?.status}`);
|
||||
console.error(`URL: ${error.config?.url}`);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
const errorBody = error instanceof Error ? error.message : String(error);
|
||||
console.error(errorBody);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up temp config file, keep report
|
||||
const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json');
|
||||
const tempConfigPath = path.join(
|
||||
process.cwd(),
|
||||
".pa11yci/config.temp.json",
|
||||
);
|
||||
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
+1
-1
@@ -14,5 +14,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "tests", "tests_bak"]
|
||||
"exclude": ["node_modules", "tests", "tests_bak", "scripts"]
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@@ -16,11 +17,11 @@ export default defineConfig({
|
||||
},
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup.tsx'],
|
||||
alias: {
|
||||
'next/server': 'next/server.js',
|
||||
'@payload-config': new URL('./tests/__mocks__/payload-config.ts', import.meta.url).pathname,
|
||||
'@': new URL('./', import.meta.url).pathname,
|
||||
},
|
||||
alias: [
|
||||
{ find: 'next/server', replacement: 'next/server.js' },
|
||||
{ find: '@payload-config', replacement: fileURLToPath(new URL('./tests/__mocks__/payload-config.ts', import.meta.url)) },
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./', import.meta.url)) },
|
||||
],
|
||||
exclude: ['**/node_modules/**', '**/.next/**'],
|
||||
server: {
|
||||
deps: {
|
||||
|
||||
Reference in New Issue
Block a user