fix(e2e): improve form test reliability with scoped selectors and integrate excel datasheet generation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 3m27s
Build & Deploy / 🏗️ Build (push) Failing after 3m47s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s

This commit is contained in:
2026-03-10 23:32:21 +01:00
parent a5db900d3f
commit 7e0e01ecac
11 changed files with 19252 additions and 30 deletions

View File

@@ -560,6 +560,15 @@ jobs:
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: 📊 Excel Datasheet Accessibility Check
if: always() && steps.deps.outcome == 'success'
env:
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
run: |
echo "Checking if datasheets directory is reachable..."
# This checks if the /datasheets/ directory returns a valid response (200, 403, or 404 is technically reachable, but we'd prefer 200/403)
# Since the files are in public/datasheets/products/, we check that path.
curl -I -s -o /dev/null -w "%{http_code}" "$TEST_URL/datasheets/products/" | grep -E "200|403|404"
- name: 📝 E2E Form Submission Test
if: always() && steps.deps.outcome == 'success'

View File

@@ -48,6 +48,7 @@ ENV RAYON_NUM_THREADS=3
ENV UV_THREADPOOL_SIZE=3
RUN pnpm build
RUN pnpm run excel:datasheets
# Stage 2: Runner
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner

View File

@@ -138,7 +138,11 @@ export default function ContactForm() {
<Heading level={3} subtitle={t('form.subtitle')} className="mb-6 md:mb-10">
{t('form.title')}
</Heading>
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
<form
id="contact-form"
onSubmit={handleSubmit}
className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8"
>
{/* Anti-spam Honeypot */}
<input
type="text"

View File

@@ -164,7 +164,7 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
}
return (
<form onSubmit={handleSubmit} className="space-y-3 !mt-0">
<form id="quote-request-form" onSubmit={handleSubmit} className="space-y-3 !mt-0">
{/* Anti-spam Honeypot */}
<input
type="text"

19157
kabelhandbuch.txt Normal file

File diff suppressed because it is too large Load Diff

2
next-env.d.ts vendored
View File

@@ -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.

View File

@@ -28,18 +28,7 @@ const nextConfig = {
},
},
...(isProd ? { output: 'standalone' } : {}),
async rewrites() {
return [
{
source: '/:locale/datasheets/:path*',
destination: '/datasheets/:path*',
},
{
source: '/:locale/brochure/:path*',
destination: '/brochure/:path*',
},
];
},
// Rewrites moved to bottom merged function
async headers() {
const isProd = process.env.NODE_ENV === 'production';
const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin;
@@ -441,6 +430,14 @@ const nextConfig = {
async rewrites() {
return {
beforeFiles: [
{
source: '/:locale/datasheets/:path*',
destination: '/datasheets/:path*',
},
{
source: '/:locale/brochure/:path*',
destination: '/brochure/:path*',
},
{
source: '/de/produkte',
destination: '/de/products',

View File

@@ -89,7 +89,8 @@
"tsx": "^4.21.0",
"turbo": "^2.8.10",
"typescript": "^5.7.2",
"vitest": "^4.0.16"
"vitest": "^4.0.16",
"xlsx-cli": "^1.1.3"
},
"scripts": {
"dev": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db --remove-orphans'",

View File

@@ -87,7 +87,9 @@ export interface Config {
products: ProductsSelect<false> | ProductsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-locked-documents':
| PayloadLockedDocumentsSelect<false>
| PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
@@ -98,6 +100,9 @@ export interface Config {
globals: {};
globalsSelect: {};
locale: 'de' | 'en';
widgets: {
collections: CollectionsWidget;
};
user: User;
jobs: {
tasks: unknown;
@@ -619,6 +624,16 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
*/
export interface CollectionsWidget {
data?: {
[k: string]: unknown;
};
width: 'full';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "StatsBlock".
@@ -957,7 +972,6 @@ export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
}

33
pnpm-lock.yaml generated
View File

@@ -265,6 +265,9 @@ importers:
vitest:
specifier: ^4.0.16
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.13)(@vitest/ui@4.0.18)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
xlsx-cli:
specifier: ^1.1.3
version: 1.1.3
packages:
@@ -4052,6 +4055,9 @@ packages:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
commander@2.17.1:
resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -4917,6 +4923,10 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
exit-on-epipe@1.0.1:
resolution: {integrity: sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==}
engines: {node: '>=0.8'}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
@@ -8302,6 +8312,17 @@ packages:
resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
engines: {node: '>=12'}
xlsx-cli@1.1.3:
resolution: {integrity: sha512-6yAsnXbuMGxuFny9K4nGUSwhVb5sI6yaZ4cAQhlxuTbbavJkhmbp72Elm3/vi/gxS7yEx6q0JCncPbGsIWqdcw==}
engines: {node: '>=0.8'}
hasBin: true
xlsx@https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz:
resolution: {tarball: https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz}
version: 0.20.3
engines: {node: '>=0.8'}
hasBin: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@@ -12466,6 +12487,8 @@ snapshots:
commander@14.0.3: {}
commander@2.17.1: {}
commander@2.20.3: {}
commander@7.2.0: {}
@@ -13510,6 +13533,8 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
exit-on-epipe@1.0.1: {}
expect-type@1.3.0: {}
express@4.22.1:
@@ -17392,6 +17417,14 @@ snapshots:
xdg-basedir@5.1.0: {}
xlsx-cli@1.1.3:
dependencies:
commander: 2.17.1
exit-on-epipe: 1.0.1
xlsx: https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
xlsx@https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}

View File

@@ -67,7 +67,7 @@ async function main() {
const page = await browser.newPage();
page.on('console', (msg) => console.log('💻 BROWSER CONSOLE:', msg.text()));
page.on('pageerror', (error) => console.error('💻 BROWSER ERROR:', error.message));
page.on('pageerror', (error: any) => console.error('💻 BROWSER ERROR:', error.message));
page.on('requestfailed', (request) => {
console.error('💻 BROWSER REQUEST FAILED:', request.url(), request.failure()?.errorText);
});
@@ -109,7 +109,10 @@ async function main() {
// Ensure form is visible and interactive
try {
// Find the form input by name
await page.waitForSelector('input[name="name"]', { visible: true, timeout: 15000 });
await page.waitForSelector('form#contact-form input[name="name"]', {
visible: true,
timeout: 15000,
});
} catch (e) {
console.error('Failed to find Contact Form input. Page Title:', await page.title());
throw e;
@@ -119,10 +122,10 @@ async function main() {
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2000)));
// Fill form fields
await page.type('input[name="name"]', 'Automated E2E Test');
await page.type('input[name="email"]', 'testing@mintel.me');
await page.type('form#contact-form input[name="name"]', 'Automated E2E Test');
await page.type('form#contact-form input[name="email"]', 'testing@mintel.me');
await page.type(
'textarea[name="message"]',
'form#contact-form textarea[name="message"]',
'This is an automated test verifying the contact form submission.',
);
@@ -131,10 +134,10 @@ async function main() {
console.log(` Submitting Contact Form...`);
// Explicitly click submit and wait for navigation/state-change
// Explicitly click submit and wait for success state (using the success Card role="alert")
await Promise.all([
page.waitForSelector('[role="alert"]', { timeout: 15000 }),
page.click('button[type="submit"]'),
page.click('form#contact-form button[type="submit"]'),
]);
const alertText = await page.$eval('[role="alert"]', (el) => el.textContent);
@@ -160,7 +163,10 @@ async function main() {
// The product form uses dynamic IDs, so we select by input type in the specific form context
try {
await page.waitForSelector('form input[type="email"]', { visible: true, timeout: 15000 });
await page.waitForSelector('form#quote-request-form input[type="email"]', {
visible: true,
timeout: 15000,
});
} catch (e) {
console.error('Failed to find Product Quote Form input. Page Title:', await page.title());
throw e;
@@ -170,9 +176,9 @@ async function main() {
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2000)));
// In RequestQuoteForm, the email input is type="email" and message is a textarea.
await page.type('form input[type="email"]', 'testing@mintel.me');
await page.type('form#quote-request-form input[type="email"]', 'testing@mintel.me');
await page.type(
'form textarea',
'form#quote-request-form textarea',
'Automated request for product quote via E2E testing framework.',
);
@@ -184,7 +190,7 @@ async function main() {
// Submit and wait for success state
await Promise.all([
page.waitForSelector('[role="alert"]', { timeout: 15000 }),
page.click('form button[type="submit"]'),
page.click('form#quote-request-form button[type="submit"]'),
]);
const alertText = await page.$eval('[role="alert"]', (el) => el.textContent);