From afd4a73a05e73b98d3a8a29eb4e43e661e41a5fe Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 4 May 2026 21:15:36 +0200 Subject: [PATCH] refactor: simplify tests and consolidate CI/CD to minimalist smoke tests --- .gitea/workflows/ci.yml | 7 -- .gitea/workflows/deploy.yml | 20 +--- .gitea/workflows/qa.yml | 203 +++--------------------------------- package.json | 12 +-- scripts/smoke.ts | 78 ++++++++++++++ 5 files changed, 94 insertions(+), 226 deletions(-) create mode 100644 scripts/smoke.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 16b6b8ab..76735477 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -42,10 +42,3 @@ jobs: - name: 🏗️ Build run: pnpm build - - - name: ♿ Accessibility Check - run: pnpm start-server-and-test start http://localhost:3000 "pnpm check:a11y http://localhost:3000" - - - name: ♿ WCAG Sitemap Audit - run: pnpm start-server-and-test start http://localhost:3000 "pnpm run check:wcag http://localhost:3000" -# monitor trigger diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e6eb3e39..316094d0 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -431,28 +431,14 @@ jobs: - - name: 🚀 OG Image Check - if: always() && steps.deps.outcome == 'success' - env: - TEST_URL: ${{ needs.prepare.outputs.next_public_url }} - run: pnpm run check:og - - name: 🌐 Core Smoke Tests (HTTP, API, Locale) - if: always() && steps.deps.outcome == 'success' - uses: https://git.infra.mintel.me/mmintel/at-mintel/.gitea/actions/core-smoke-tests@main - with: - TARGET_URL: ${{ needs.prepare.outputs.next_public_url }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} - UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} - - - name: 📝 E2E Form Submission Test + # ── Minimalist Smoke Test ────────────────────────────────────────── + - name: 🌐 Smoke Test (Essential Flow) if: always() && steps.deps.outcome == 'success' env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} - PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }} PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium - run: pnpm run check:forms + run: npx tsx scripts/smoke.ts # ────────────────────────────────────────────────────────────────────────────── # JOB 7: Notifications diff --git a/.gitea/workflows/qa.yml b/.gitea/workflows/qa.yml index 9ecb313b..c3f6631c 100644 --- a/.gitea/workflows/qa.yml +++ b/.gitea/workflows/qa.yml @@ -10,11 +10,8 @@ env: PROJECT_NAME: 'klz-2026' jobs: - # ──────────────────────────────────────────────────── - # 1. Static Checks (HTML, Assets, HTTP) - # ──────────────────────────────────────────────────── - static: - name: 🔍 Static Analysis + smoke: + name: 💨 Smoke & Health runs-on: docker container: image: catthehacker/ubuntu:act-latest @@ -38,199 +35,23 @@ jobs: key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} - name: Install if: steps.cache-deps.outputs.cache-hit != 'true' - run: | - pnpm store prune - pnpm install --no-frozen-lockfile + run: pnpm install --no-frozen-lockfile - name: 🌐 Install Chrome & Dependencies run: | apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 npx puppeteer browsers install chrome - - name: 🌐 HTML Validation + - name: 🚀 Run Smoke Test env: NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:html - - name: 🖼️ Broken Assets - env: - NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - ASSET_CHECK_LIMIT: 10 - run: pnpm run check:assets - - name: 🔒 HTTP Headers - env: - NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:http - - # ──────────────────────────────────────────────────── - # 2. Accessibility (WCAG) - # ──────────────────────────────────────────────────── - a11y: - name: ♿ Accessibility - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: 🔐 Registry Auth + PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium + run: npx tsx scripts/smoke.ts + - name: 🔔 Notify + if: always() run: | - echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc - echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc - - name: 📦 Cache node_modules - uses: actions/cache@v4 - id: cache-deps - with: - path: node_modules - key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} - - name: Install - if: steps.cache-deps.outputs.cache-hit != 'true' - run: | - pnpm store prune - pnpm install --no-frozen-lockfile - - name: 🌐 Install Chrome & Dependencies - run: | - apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 - npx puppeteer browsers install chrome - - name: ♿ WCAG Scan - continue-on-error: true - env: - NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - run: pnpm run check:wcag - - # ──────────────────────────────────────────────────── - # 3. Performance (Lighthouse) - # ──────────────────────────────────────────────────── - lighthouse: - name: 🎭 Lighthouse - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: 🔐 Registry Auth - run: | - echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc - echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc - - name: 📦 Cache node_modules - uses: actions/cache@v4 - id: cache-deps - with: - path: node_modules - key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} - - name: Install - if: steps.cache-deps.outputs.cache-hit != 'true' - run: | - pnpm store prune - pnpm install --no-frozen-lockfile - - name: 🌐 Install Chrome & Dependencies - run: | - apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 - npx puppeteer browsers install chrome - - name: 🎭 Desktop - env: - NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - PAGESPEED_LIMIT: 5 - run: pnpm run pagespeed:test -- --collect.settings.preset=desktop - - name: 📱 Mobile - env: - NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} - PAGESPEED_LIMIT: 5 - run: pnpm run pagespeed:test -- --collect.settings.preset=mobile - - # ──────────────────────────────────────────────────── - # 4. Link Check & Dependency Audit - # ──────────────────────────────────────────────────── - links: - name: 🔗 Links & Deps - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: 🔐 Registry Auth - run: | - echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc - echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc - - name: 📦 Cache node_modules - uses: actions/cache@v4 - id: cache-deps - with: - path: node_modules - key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} - - name: Install - if: steps.cache-deps.outputs.cache-hit != 'true' - run: | - pnpm store prune - pnpm install --no-frozen-lockfile - - name: 📦 Depcheck - continue-on-error: true - run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" || true - - name: 🔗 Lychee Link Check - uses: lycheeverse/lychee-action@v2 - with: - args: --accept 200,204,429 --timeout 15 --insecure --exclude "file://*" --exclude "https://logs.infra.***.me/*" --exclude "https://git.infra.***.me/*" --exclude "https://umami.is/docs/best-practices" --exclude "https://***/*" . - fail: true - - # ──────────────────────────────────────────────────── - # 5. Notification - # ──────────────────────────────────────────────────── - notify: - name: 🔔 Notify - needs: [static, a11y, lighthouse, links] - if: always() - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - steps: - - name: 🔔 Gotify - shell: bash - run: | - STATIC="${{ needs.static.result }}" - A11Y="${{ needs.a11y.result }}" - LIGHTHOUSE="${{ needs.lighthouse.result }}" - LINKS="${{ needs.links.result }}" - - if [[ "$STATIC" != "success" || "$LIGHTHOUSE" != "success" ]]; then - PRIORITY=8 - EMOJI="🚨" - STATUS="Failed" - else - PRIORITY=2 - EMOJI="✅" - STATUS="Passed" - fi - - TITLE="$EMOJI ${{ env.PROJECT_NAME }} QA $STATUS" - MESSAGE="Static: $STATIC | A11y: $A11Y | Lighthouse: $LIGHTHOUSE | Links: $LINKS - ${{ env.TARGET_URL }}" - - if [[ -z "${{ secrets.GOTIFY_URL }}" || -z "${{ secrets.GOTIFY_TOKEN }}" ]]; then - echo "⚠️ Gotify credentials missing, skipping notification." - exit 0 - fi - + STATUS="${{ job.status }}" + TITLE="💨 ${{ env.PROJECT_NAME }} Nightly Smoke $STATUS" curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=$TITLE" \ - -F "message=$MESSAGE" \ - -F "priority=$PRIORITY" || true + -F "message=Status: $STATUS | URL: ${{ env.TARGET_URL }}" \ + -F "priority=5" || true diff --git a/package.json b/package.json index b2580169..e6d6732a 100644 --- a/package.json +++ b/package.json @@ -95,18 +95,8 @@ "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", "test:og": "vitest run tests/og-image.test.ts", - "check:og": "tsx scripts/check-og-images.ts", - "check:a11y": "pa11y-ci", - "check:wcag": "tsx ./scripts/wcag-sitemap.ts", - "check:html": "tsx ./scripts/check-html.ts", - "check:http": "tsx ./scripts/check-http.ts", - "check:locale": "tsx ./scripts/check-locale.ts", + "check:smoke": "tsx scripts/smoke.ts", "check:spell": "cspell \"content/**/*.{md,mdx}\" \"app/**/*.tsx\" \"components/**/*.tsx\"", - "check:security": "tsx ./scripts/check-security.ts", - "check:links": "bash ./scripts/check-links.sh", - "check:assets": "tsx ./scripts/check-broken-assets.ts", - "check:forms": "tsx ./scripts/check-forms.ts", - "check:apis": "tsx ./scripts/check-apis.ts", "pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts", "pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts", "assets:push:testing": "bash ./scripts/assets-sync.sh local testing", diff --git a/scripts/smoke.ts b/scripts/smoke.ts new file mode 100644 index 00000000..d3e41599 --- /dev/null +++ b/scripts/smoke.ts @@ -0,0 +1,78 @@ +import puppeteer from 'puppeteer'; + +const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026'; + +async function run() { + console.log(`🚀 Smoke Test: ${targetUrl}`); + + const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + + try { + // 1. Login via Gatekeeper + console.log('🔑 Authenticating...'); + await page.goto(targetUrl, { waitUntil: 'networkidle2' }); + + const isGatekeeper = await page.$('input[name="password"]'); + if (isGatekeeper) { + await page.type('input[name="password"]', gatekeeperPassword); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle2' }), + page.click('button[type="submit"]'), + ]); + } + + // 2. Check Key Pages & Assets + const pages = ['/', '/de/kontakt', '/en/contact', '/de/blog']; + for (const p of pages) { + console.log(`📄 Checking ${p}...`); + const res = await page.goto(`${targetUrl}${p}`, { waitUntil: 'domcontentloaded' }); + if (res?.status() !== 200) throw new Error(`Page ${p} returned ${res?.status()}`); + + // Check for broken images on this page + const images = await page.$$eval('img', (imgs) => imgs.map((img) => img.src)); + console.log(` Checking ${images.length} images...`); + for (const src of images.slice(0, 5)) { + // Check first 5 images per page + const imgRes = await page.goto(src, { waitUntil: 'domcontentloaded' }).catch(() => null); + if (imgRes && imgRes.status() >= 400) + console.warn(` ⚠️ Broken image: ${src} (${imgRes.status()})`); + } + await page.goto(`${targetUrl}${p}`, { waitUntil: 'domcontentloaded' }); // Go back + } + + // 3. Test Contact Form + console.log('📝 Testing Contact Form...'); + await page.goto(`${targetUrl}/de/kontakt`, { waitUntil: 'networkidle2' }); + + await page.type('input[name="name"]', 'Smoke Test'); + await page.type('input[name="email"]', 'smoke@mintel.me'); + await page.type('textarea[name="message"]', 'Automated smoke test submission.'); + + await Promise.all([ + page.waitForSelector('[role="alert"]', { timeout: 10000 }), + page.click('button[type="submit"]'), + ]); + + const alertText = await page.$eval('[role="alert"]', (el) => el.textContent); + console.log(`🔔 Alert: ${alertText}`); + + if (alertText?.toLowerCase().includes('error') || alertText?.toLowerCase().includes('fehler')) { + throw new Error(`Form submission failed: ${alertText}`); + } + + console.log('✅ Smoke test passed!'); + process.exit(0); + } catch (err: any) { + console.error(`❌ Smoke test failed: ${err.message}`); + process.exit(1); + } finally { + await browser.close(); + } +} + +run();