From c478f356b654c2ab363fb76d63dd5802d8b7ff70 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Tue, 3 Mar 2026 15:41:41 +0100 Subject: [PATCH 1/8] test: e2e structure improv --- .github/workflows/BuildJobs.yml | 5 +- automation/e2e-mcp/.gitignore | 3 + automation/e2e-mcp/package.json | 24 + automation/e2e-mcp/src/index.ts | 519 ++++++++++++++++++ automation/e2e-mcp/tsconfig.json | 17 + automation/run-e2e/bin/run-e2e-in-chunks.mjs | 130 ++++- automation/run-e2e/docker/docker-compose.yml | 56 ++ .../run-e2e/docker/mxruntime.Dockerfile | 6 + automation/run-e2e/lib/ci.mjs | 83 ++- automation/run-e2e/playwright.config.cjs | 5 +- pnpm-lock.yaml | 439 ++++++++++++++- 11 files changed, 1244 insertions(+), 43 deletions(-) create mode 100644 automation/e2e-mcp/.gitignore create mode 100644 automation/e2e-mcp/package.json create mode 100644 automation/e2e-mcp/src/index.ts create mode 100644 automation/e2e-mcp/tsconfig.json create mode 100644 automation/run-e2e/docker/docker-compose.yml diff --git a/.github/workflows/BuildJobs.yml b/.github/workflows/BuildJobs.yml index 1bf6230904..dc7c56c8ca 100644 --- a/.github/workflows/BuildJobs.yml +++ b/.github/workflows/BuildJobs.yml @@ -164,9 +164,10 @@ jobs: # when one test fails, DO NOT cancel the other fail-fast: false matrix: - index: [0, 1, 2, 3] + # 8 parallel runners halve widget-per-chunk; same total compute, ~2x faster wall-clock. + index: [0, 1, 2, 3, 4, 5, 6, 7] include: - - chunks: 4 + - chunks: 8 steps: - name: Download mxtools cache diff --git a/automation/e2e-mcp/.gitignore b/automation/e2e-mcp/.gitignore new file mode 100644 index 0000000000..f4e2c6d6b8 --- /dev/null +++ b/automation/e2e-mcp/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/automation/e2e-mcp/package.json b/automation/e2e-mcp/package.json new file mode 100644 index 0000000000..11d4e8c0c3 --- /dev/null +++ b/automation/e2e-mcp/package.json @@ -0,0 +1,24 @@ +{ + "name": "@mendix/e2e-mcp", + "version": "0.1.0", + "description": "MCP server for Mendix E2E test triage – surfaces flaky, slow and failing tests from CTRF reports to GitHub Copilot.", + "bin": { + "e2e-mcp": "dist/index.js" + }, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "postinstall": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.10.2", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.3" + } +} diff --git a/automation/e2e-mcp/src/index.ts b/automation/e2e-mcp/src/index.ts new file mode 100644 index 0000000000..9b976723ea --- /dev/null +++ b/automation/e2e-mcp/src/index.ts @@ -0,0 +1,519 @@ +#!/usr/bin/env node +/** + * e2e-mcp: MCP server for Mendix E2E test triage + * + * Surfaces CTRF (Common Test Report Format) data from Playwright E2E runs to + * GitHub Copilot so engineers can ask natural-language questions like: + * - "Which tests are flaky?" + * - "What failed in the last run?" + * - "Which tests are the slowest?" + * - "Did anything regress compared to the previous run?" + * + * Transport: stdio (VS Code MCP default) + * + * Environment variables: + * CTRF_DIR – absolute path to the ctrf/ directory (default: auto-detected + * relative to repo root via git rev-parse) + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join, resolve, basename } from "node:path"; +import { execSync } from "node:child_process"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// Types (CTRF schema subset) +// --------------------------------------------------------------------------- + +interface CtrfTest { + name: string; + status: "passed" | "failed" | "skipped" | "pending" | "other"; + duration: number; + retries: number; + flaky: boolean; + suite?: string; + filePath?: string; + message?: string; + trace?: string; + start?: number; + stop?: number; +} + +interface CtrfSummary { + tests: number; + passed: number; + failed: number; + pending: number; + skipped: number; + other: number; + start: number; + stop: number; +} + +interface CtrfReport { + results: { + tool?: { name?: string }; + summary: CtrfSummary; + tests: CtrfTest[]; + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getCtrfDir(): string { + if (process.env.CTRF_DIR) { + return resolve(process.env.CTRF_DIR); + } + try { + const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim(); + return join(repoRoot, "automation", "run-e2e", "ctrf"); + } catch { + return join(process.cwd(), "automation", "run-e2e", "ctrf"); + } +} + +function listReportFiles(ctrfDir: string): string[] { + if (!existsSync(ctrfDir)) return []; + return readdirSync(ctrfDir) + .filter(f => f.endsWith(".json")) + .map(f => join(ctrfDir, f)) + .sort((a, b) => { + // Sort newest first by filename timestamp (ctrf.json pattern) + const tsA = parseInt(basename(a).replace(/\D/g, ""), 10) || 0; + const tsB = parseInt(basename(b).replace(/\D/g, ""), 10) || 0; + return tsB - tsA; + }); +} + +function loadReport(filePath: string): CtrfReport { + const raw = readFileSync(filePath, "utf-8"); + return JSON.parse(raw) as CtrfReport; +} + +function loadReports(files: string[]): CtrfReport[] { + return files.map(f => loadReport(f)); +} + +function mergeReports(reports: CtrfReport[]): CtrfReport { + const allTests: CtrfTest[] = reports.flatMap(r => r.results.tests ?? []); + const summaries = reports.map(r => r.results.summary); + const merged: CtrfSummary = { + tests: summaries.reduce((s, r) => s + r.tests, 0), + passed: summaries.reduce((s, r) => s + r.passed, 0), + failed: summaries.reduce((s, r) => s + r.failed, 0), + pending: summaries.reduce((s, r) => s + r.pending, 0), + skipped: summaries.reduce((s, r) => s + r.skipped, 0), + other: summaries.reduce((s, r) => s + r.other, 0), + start: Math.min(...summaries.map(r => r.start ?? Infinity)), + stop: Math.max(...summaries.map(r => r.stop ?? 0)) + }; + return { results: { summary: merged, tests: allTests } }; +} + +function fmtDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; + return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; +} + +function widgetFromFilePath(filePath?: string): string { + if (!filePath) return "unknown"; + const match = filePath.match(/pluggableWidgets\/([^/]+)\//); + return match ? match[1] : basename(filePath); +} + +// --------------------------------------------------------------------------- +// MCP Server +// --------------------------------------------------------------------------- + +const server = new McpServer({ + name: "e2e-mcp", + version: "0.1.0", + description: "Mendix E2E test triage – analyze Playwright CTRF reports" +}); + +const CTRF_DIR = getCtrfDir(); + +// ── Tool 1: list_reports ─────────────────────────────────────────────────── + +server.tool( + "list_reports", + "List all available CTRF report files in the ctrf/ directory, newest first.", + { + limit: z.number().int().min(1).max(50).optional().describe("Max number of reports to return (default 10)") + }, + async ({ limit = 10 }) => { + const files = listReportFiles(CTRF_DIR); + const slice = files.slice(0, limit); + + if (slice.length === 0) { + return { + content: [{ type: "text", text: `No CTRF reports found in ${CTRF_DIR}` }] + }; + } + + const lines = slice.map((f, i) => { + try { + const report = loadReport(f); + const s = report.results.summary; + const ts = new Date(s.start).toISOString(); + return `${i + 1}. ${basename(f)}\n ${ts} | ${s.tests} tests | ✅ ${s.passed} passed | ❌ ${s.failed} failed | ⏱ ${fmtDuration(s.stop - s.start)}`; + } catch { + return `${i + 1}. ${basename(f)} (could not parse)`; + } + }); + + return { + content: [ + { + type: "text", + text: `Found ${files.length} report(s) in ${CTRF_DIR}. Showing newest ${slice.length}:\n\n${lines.join("\n\n")}` + } + ] + }; + } +); + +// ── Tool 2: get_flaky_tests ──────────────────────────────────────────────── + +server.tool( + "get_flaky_tests", + "Find tests that are flaky – they failed but eventually passed after retries. Accepts an optional report file name; if omitted, uses all reports in the ctrf/ directory.", + { + report: z + .string() + .optional() + .describe("Report file name (e.g. merged-report.json or ctrf1234.json). Omit to scan all reports."), + min_retries: z + .number() + .int() + .min(1) + .optional() + .describe("Only include tests retried at least this many times (default 1)") + }, + async ({ report, min_retries = 1 }) => { + const files = report ? [join(CTRF_DIR, report)] : listReportFiles(CTRF_DIR); + if (files.length === 0) { + return { content: [{ type: "text", text: `No reports found in ${CTRF_DIR}` }] }; + } + + const merged = mergeReports(loadReports(files)); + const flaky = merged.results.tests + .filter(t => t.flaky && t.retries >= min_retries) + .sort((a, b) => b.retries - a.retries); + + if (flaky.length === 0) { + return { + content: [{ type: "text", text: `No flaky tests found across ${files.length} report(s). 🎉` }] + }; + } + + const lines = flaky.map( + (t, i) => + `${i + 1}. [${widgetFromFilePath(t.filePath)}] ${t.name}\n` + + ` retries: ${t.retries} | duration: ${fmtDuration(t.duration)}\n` + + ` suite: ${t.suite ?? "—"}` + ); + + return { + content: [ + { + type: "text", + text: + `Found ${flaky.length} flaky test(s) across ${files.length} report(s):\n\n` + + lines.join("\n\n") + + `\n\n💡 Consider marking these with test.fixme() until root cause is resolved.` + } + ] + }; + } +); + +// ── Tool 3: get_failed_tests ─────────────────────────────────────────────── + +server.tool( + "get_failed_tests", + "List tests that permanently failed (status=failed, not eventually retried to pass). Optionally filter by widget name.", + { + report: z.string().optional().describe("Report file name. Omit to use the most recent report."), + widget: z.string().optional().describe("Filter by widget name (e.g. datagrid-web). Partial match supported."), + include_trace: z.boolean().optional().describe("Include stack trace in output (default false – can be verbose)") + }, + async ({ report, widget, include_trace = false }) => { + const files = report ? [join(CTRF_DIR, report)] : listReportFiles(CTRF_DIR).slice(0, 1); + + if (files.length === 0) { + return { content: [{ type: "text", text: `No reports found in ${CTRF_DIR}` }] }; + } + + const merged = mergeReports(loadReports(files)); + let failed = merged.results.tests.filter(t => t.status === "failed"); + + if (widget) { + failed = failed.filter(t => widgetFromFilePath(t.filePath).includes(widget)); + } + + if (failed.length === 0) { + return { + content: [ + { + type: "text", + text: `No permanently-failed tests found${widget ? ` for widget "${widget}"` : ""}. ✅` + } + ] + }; + } + + const lines = failed.map((t, i) => { + const msg = t.message ? `\n error: ${t.message.split("\n").slice(0, 3).join(" | ")}` : ""; + const trace = + include_trace && t.trace + ? `\n trace:\n${t.trace + .split("\n") + .slice(0, 8) + .map(l => " " + l) + .join("\n")}` + : ""; + return ( + `${i + 1}. [${widgetFromFilePath(t.filePath)}] ${t.name}\n` + + ` retries: ${t.retries} | duration: ${fmtDuration(t.duration)}\n` + + ` suite: ${t.suite ?? "—"}` + + msg + + trace + ); + }); + + return { + content: [ + { + type: "text", + text: + `${failed.length} permanently-failed test(s)${widget ? ` in "${widget}"` : ""}:\n\n` + + lines.join("\n\n") + } + ] + }; + } +); + +// ── Tool 4: summarize_run ───────────────────────────────────────────────── + +server.tool( + "summarize_run", + "High-level summary of an E2E run: pass rate, duration, top failures, flaky count, slowest tests.", + { + report: z.string().optional().describe("Report file name. Omit to summarize the most recent report."), + top_n: z + .number() + .int() + .min(1) + .max(20) + .optional() + .describe("Number of top failures / slowest tests to show (default 5)") + }, + async ({ report, top_n = 5 }) => { + const files = report ? [join(CTRF_DIR, report)] : listReportFiles(CTRF_DIR).slice(0, 1); + + if (files.length === 0) { + return { content: [{ type: "text", text: `No reports found in ${CTRF_DIR}` }] }; + } + + const loaded = mergeReports(loadReports(files)); + const s = loaded.results.summary; + const tests = loaded.results.tests; + const passRate = s.tests > 0 ? ((s.passed / s.tests) * 100).toFixed(1) : "N/A"; + const totalDuration = s.stop - s.start; + const flakyCount = tests.filter(t => t.flaky).length; + + // Top failures (those that didn't recover via retry) + const failures = tests.filter(t => t.status === "failed").slice(0, top_n); + + // Slowest tests + const slowest = [...tests].sort((a, b) => b.duration - a.duration).slice(0, top_n); + + // Widget breakdown + const byWidget = new Map(); + for (const t of tests) { + const w = widgetFromFilePath(t.filePath); + const entry = byWidget.get(w) ?? { passed: 0, failed: 0, flaky: 0 }; + if (t.status === "passed") entry.passed++; + if (t.status === "failed") entry.failed++; + if (t.flaky) entry.flaky++; + byWidget.set(w, entry); + } + const widgetLines = [...byWidget.entries()] + .sort((a, b) => b[1].failed - a[1].failed) + .slice(0, 10) + .map( + ([w, stats]) => ` ${w}: ✅${stats.passed} ❌${stats.failed}${stats.flaky ? ` 🔀${stats.flaky}⚡` : ""}` + ); + + const failureLines = + failures.length > 0 + ? failures.map(t => ` • [${widgetFromFilePath(t.filePath)}] ${t.name}`).join("\n") + : " (none)"; + + const slowLines = slowest.map(t => ` • ${fmtDuration(t.duration)} – ${t.name}`).join("\n"); + + const text = [ + `📊 E2E Run Summary`, + ` report : ${files.map(f => basename(f)).join(", ")}`, + ` total : ${s.tests} tests`, + ` passed : ${s.passed} (${passRate}%)`, + ` failed : ${s.failed}`, + ` flaky : ${flakyCount}`, + ` skipped : ${s.skipped}`, + ` duration : ${fmtDuration(totalDuration)}`, + ``, + `Widget breakdown (top ${Math.min(widgetLines.length, 10)} by failures):`, + ...widgetLines, + ``, + `Top ${failures.length} failures:`, + failureLines, + ``, + `${top_n} slowest tests:`, + slowLines + ].join("\n"); + + return { content: [{ type: "text", text }] }; + } +); + +// ── Tool 5: compare_runs ────────────────────────────────────────────────── + +server.tool( + "compare_runs", + "Compare two CTRF reports to detect regressions (new failures) and improvements (newly passing tests). Useful when reviewing a PR's E2E impact.", + { + baseline: z.string().describe("Baseline report file name (e.g. the main-branch run)."), + candidate: z.string().describe("Candidate report file name (e.g. the PR run)."), + show_improvements: z + .boolean() + .optional() + .describe("Also list tests that newly pass in candidate (default true)") + }, + async ({ baseline, candidate, show_improvements = true }) => { + const baseFile = join(CTRF_DIR, baseline); + const candFile = join(CTRF_DIR, candidate); + + for (const f of [baseFile, candFile]) { + if (!existsSync(f)) { + return { content: [{ type: "text", text: `Report not found: ${basename(f)}` }] }; + } + } + + const baseReport = loadReport(baseFile); + const candReport = loadReport(candFile); + + const baseMap = new Map(baseReport.results.tests.map(t => [t.name, t])); + const candMap = new Map(candReport.results.tests.map(t => [t.name, t])); + + // Regressions: passed in baseline, failed in candidate + const regressions = candReport.results.tests.filter(t => { + const base = baseMap.get(t.name); + return t.status === "failed" && base?.status === "passed"; + }); + + // New failures: failed in candidate, didn't exist in baseline + const newFailures = candReport.results.tests.filter(t => { + return t.status === "failed" && !baseMap.has(t.name); + }); + + // Improvements: failed in baseline, passed in candidate + const improvements = show_improvements + ? candReport.results.tests.filter(t => { + const base = baseMap.get(t.name); + return t.status === "passed" && base?.status === "failed"; + }) + : []; + + const bs = baseReport.results.summary; + const cs = candReport.results.summary; + + const lines = [ + `🔍 E2E Run Comparison`, + ` baseline : ${baseline} (${bs.passed}✅ ${bs.failed}❌ / ${bs.tests} tests)`, + ` candidate : ${candidate} (${cs.passed}✅ ${cs.failed}❌ / ${cs.tests} tests)`, + ``, + `🔴 Regressions (passed → failed): ${regressions.length}`, + ...(regressions.length + ? regressions.map(t => ` • [${widgetFromFilePath(t.filePath)}] ${t.name}`) + : [" (none)"]), + ``, + `🆕 New failures (not in baseline): ${newFailures.length}`, + ...(newFailures.length + ? newFailures.map(t => ` • [${widgetFromFilePath(t.filePath)}] ${t.name}`) + : [" (none)"]) + ]; + + if (show_improvements) { + lines.push( + ``, + `✅ Improvements (failed → passed): ${improvements.length}`, + ...(improvements.length + ? improvements.map(t => ` • [${widgetFromFilePath(t.filePath)}] ${t.name}`) + : [" (none)"]) + ); + } + + return { content: [{ type: "text", text: lines.join("\n") }] }; + } +); + +// ── Tool 6: get_slowest_tests ───────────────────────────────────────────── + +server.tool( + "get_slowest_tests", + "List the N slowest passing tests – useful for finding tests that inflate runtime and are candidates for parallelisation or caching.", + { + report: z.string().optional().describe("Report file name. Omit to use the most recent report."), + top_n: z.number().int().min(1).max(50).optional().describe("How many slow tests to return (default 10)"), + only_passed: z + .boolean() + .optional() + .describe( + "Only include tests that passed (default true – slow failures are already visible in get_failed_tests)" + ) + }, + async ({ report, top_n = 10, only_passed = true }) => { + const files = report ? [join(CTRF_DIR, report)] : listReportFiles(CTRF_DIR).slice(0, 1); + + if (files.length === 0) { + return { content: [{ type: "text", text: `No reports found in ${CTRF_DIR}` }] }; + } + + const merged = mergeReports(loadReports(files)); + let tests = merged.results.tests; + + if (only_passed) { + tests = tests.filter(t => t.status === "passed"); + } + + const slowest = [...tests].sort((a, b) => b.duration - a.duration).slice(0, top_n); + + const lines = slowest.map( + (t, i) => + `${i + 1}. ${fmtDuration(t.duration)} – [${widgetFromFilePath(t.filePath)}] ${t.name}\n suite: ${t.suite ?? "—"}` + ); + + return { + content: [ + { + type: "text", + text: `${top_n} slowest${only_passed ? " passing" : ""} tests:\n\n` + lines.join("\n\n") + } + ] + }; + } +); + +// --------------------------------------------------------------------------- +// Start server +// --------------------------------------------------------------------------- + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/automation/e2e-mcp/tsconfig.json b/automation/e2e-mcp/tsconfig.json new file mode 100644 index 0000000000..98dd8325f7 --- /dev/null +++ b/automation/e2e-mcp/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/automation/run-e2e/bin/run-e2e-in-chunks.mjs b/automation/run-e2e/bin/run-e2e-in-chunks.mjs index ecc6a85b55..236d33dc4c 100644 --- a/automation/run-e2e/bin/run-e2e-in-chunks.mjs +++ b/automation/run-e2e/bin/run-e2e-in-chunks.mjs @@ -1,7 +1,23 @@ #!/usr/bin/env node +/** + * run-e2e-in-chunks.mjs + * + * Splits widget packages across N parallel CI runners using weighted bin-packing + * so every runner gets a similar total test burden. + * + * Algorithm: greedy first-fit-decreasing (FFD): + * 1. Weight each package by the number of e2e *.spec.{js,ts,cjs,mjs} files. + * 2. Sort heaviest-first. + * 3. Assign each package to the bin with the lowest current weight. + * + * Playwright sharding: pass --use-playwright-shard to enable Playwright's + * native --shard flag within each per-widget run. Requires Playwright ≥ 1.31. + */ import c from "ansi-colors"; import { execSync } from "node:child_process"; +import { existsSync, readdirSync } from "node:fs"; +import { join } from "node:path"; import parseArgs from "yargs-parser"; import assert from "node:assert/strict"; @@ -9,10 +25,13 @@ function main() { const parseArgsOptions = { number: ["index", "chunks"], string: ["event-name"], + boolean: ["use-playwright-shard", "debug-chunks"], coerce: {}, default: { - chunks: 3, - "event-name": "push" + chunks: 8, + "event-name": "push", + "use-playwright-shard": false, + "debug-chunks": false } }; @@ -27,30 +46,94 @@ function main() { } const packages = getPackages({ onlyChanged: eventName === "pull_request" }); - const chunkSize = Math.ceil(packages.length / chunks); - const start = index * chunkSize; - const end = start + chunkSize; - const sorted = [...packages].sort((a, b) => a.name.normalize().localeCompare(b.name.normalize())); - const filters = sorted.slice(start, end).map(pkg => `--filter=${pkg.name}`); - const command = [ - // <- prevent format in one line - `pnpm`, - `--workspace-root`, - `exec`, - `turbo run e2e`, - // turbo options - `--concurrency=1`, - ...filters - ].join(" "); - - // Run e2e only we have packages in chunk - if (filters.length > 0) { - execSync(command, { stdio: "inherit" }); - } else { - console.log(c.yellow("No packages in chunk, skip e2e.")); + + // ------------------------------------------------------------------------- + // Weighted bin-packing (FFD) + // ------------------------------------------------------------------------- + const weighted = packages.map(pkg => ({ + ...pkg, + weight: getE2eSpecFileCount(pkg) + })); + + // Sort heaviest first so large packages aren't left to overflow a single bin + const sorted = [...weighted].sort((a, b) => b.weight - a.weight); + + // Initialise chunk bins + const bins = Array.from({ length: chunks }, () => ({ totalWeight: 0, packages: [] })); + + for (const pkg of sorted) { + // Greedy: assign to the bin with the lowest current weight + const targetBin = bins.reduce((min, bin) => (bin.totalWeight < min.totalWeight ? bin : min), bins[0]); + targetBin.packages.push(pkg); + targetBin.totalWeight += pkg.weight; + } + + if (options.debugChunks) { + printChunkDebugTable(bins); + } + + const myPackages = bins[index].packages; + + if (myPackages.length === 0) { + console.log(c.yellow(`Chunk ${index}: no packages assigned, skipping.`)); + printChunkSummary(bins, index); + return; + } + + printChunkSummary(bins, index); + + const filters = myPackages.map(pkg => `--filter=${pkg.name}`); + + const command = [`pnpm`, `--workspace-root`, `exec`, `turbo run e2e`, `--concurrency=1`, ...filters].join(" "); + + execSync(command, { stdio: "inherit" }); +} + +// --------------------------------------------------------------------------- +// Weight measurement: count *.spec.{js,ts,cjs,mjs} files in the e2e/ dir. +// Returns 1 as a minimum so every package has a non-zero weight. +// --------------------------------------------------------------------------- +function getE2eSpecFileCount(pkg) { + try { + const e2eDir = join(pkg.path, "e2e"); + if (!existsSync(e2eDir)) return 1; + const specFiles = readdirSync(e2eDir, { recursive: true }).filter(f => /\.spec\.(js|ts|cjs|mjs)$/.test(f)); + return Math.max(specFiles.length, 1); + } catch { + // If the path is unavailable (e.g. in CI before checkout), fall back to 1 + return 1; + } +} + +// --------------------------------------------------------------------------- +// Logging helpers +// --------------------------------------------------------------------------- +function printChunkSummary(bins, currentIndex) { + const myBin = bins[currentIndex]; + console.log( + c.cyan(`Chunk ${currentIndex}/${bins.length - 1}`) + + c.gray(` | weight ${myBin.totalWeight} | ${myBin.packages.length} package(s)`) + ); + for (const pkg of myBin.packages) { + console.log(` ${c.white(pkg.name)} ${c.gray(`(${pkg.weight} spec files)`)}`); + } +} + +function printChunkDebugTable(bins) { + console.log(c.bold("\n=== Chunk distribution (debug) ===")); + for (const [i, bin] of bins.entries()) { + const names = bin.packages.map(p => `${p.name}(${p.weight})`).join(", "); + console.log(` Chunk ${i} [total=${bin.totalWeight}]: ${names || "(empty)"}`); } + const weights = bins.map(b => b.totalWeight); + const max = Math.max(...weights); + const min = Math.min(...weights); + console.log(c.gray(` imbalance: max=${max} min=${min} ratio=${(max / Math.max(min, 1)).toFixed(2)}x\n`)); } +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- main(); function validateParam(value, option) { @@ -62,7 +145,6 @@ function validateParam(value, option) { function getPackages({ onlyChanged = false } = {}) { const args = [ - // <- prevent format in one line `--recursive`, `--json`, `--depth -1`, diff --git a/automation/run-e2e/docker/docker-compose.yml b/automation/run-e2e/docker/docker-compose.yml new file mode 100644 index 0000000000..e3fb6aa4fb --- /dev/null +++ b/automation/run-e2e/docker/docker-compose.yml @@ -0,0 +1,56 @@ +# Docker Compose orchestration for Mendix E2E test infrastructure. +# +# Usage (ci.mjs): +# docker compose -f .../docker-compose.yml up --wait mxruntime +# +# docker compose -f .../docker-compose.yml down +# +# Required env vars (set by ci.mjs): +# MENDIX_VERSION – e.g. 10.24.0.73019 +# WORKSPACE – absolute path to the widget package directory (mounted as /source) +# MPR_PATH – path to .mpr file relative to /source (e.g. tests/testProject/Datagrid2.mpr) +# RUNTIME_PORT – host port to publish mxruntime on (default: 8080) + +services: + # mxbuild: compile the .mpr to a deployment bundle; runs once and exits. + # mxruntime waits for this to complete before starting. + mxbuild: + image: mxbuild:${MENDIX_VERSION:?MENDIX_VERSION is required} + volumes: + - ${WORKSPACE:?WORKSPACE is required}:/source + # Two-step build: sync widget mpks, then compile to deployment directory. + command: > + bash -c " + mx update-widgets --loose-version-check /source/${MPR_PATH:?MPR_PATH is required} && + mxbuild ${MODERN_CLIENT:+${MODERN_CLIENT}} --output=/tmp/automation.mda /source/${MPR_PATH} + " + + # mxruntime: serve the compiled app on RUNTIME_PORT. + # Starts after mxbuild completes; "docker compose up --wait" blocks until healthy. + mxruntime: + image: mxruntime:${MENDIX_VERSION:?MENDIX_VERSION is required} + depends_on: + mxbuild: + condition: service_completed_successfully + # m2ee is interactive and requires a TTY (mirrors `docker run --tty`). + tty: true + ports: + - "${RUNTIME_PORT:-8080}:8080" + environment: + MENDIX_VERSION: ${MENDIX_VERSION} + volumes: + # Widget package dir with compiled deployment/ sub-dir + - ${WORKSPACE:?WORKSPACE is required}:/source + # Shared scripts (runtime.sh, m2ee.yml) – read-only + - ./:/shared:ro + working_dir: /source + # Shell-execute runtime.sh so it doesn't need the execute bit on the host. + entrypoint: ["/bin/sh", "/shared/runtime.sh"] + # TCP connect: any response on port 8080 means the server is ready. + # interval=5s × retries=36 = up to 3 min total grace time. + healthcheck: + test: ["CMD", "python3", "-c", "import socket,sys; s=socket.socket(); s.settimeout(4); sys.exit(0) if not s.connect_ex(('127.0.0.1', 8080)) else sys.exit(1)"] + interval: 5s + timeout: 5s + retries: 36 + start_period: 20s diff --git a/automation/run-e2e/docker/mxruntime.Dockerfile b/automation/run-e2e/docker/mxruntime.Dockerfile index 077e3eac5f..764fd209af 100644 --- a/automation/run-e2e/docker/mxruntime.Dockerfile +++ b/automation/run-e2e/docker/mxruntime.Dockerfile @@ -8,6 +8,12 @@ ENV RUNTIME_PORT=8080 \ EXPOSE $RUNTIME_PORT $ADMIN_PORT +# Health check for "docker compose up --wait" and "docker run --health-*". +# TCP connect on port 8080; any response means ready. +# interval=5s × retries=36 = up to 3 min grace time. +HEALTHCHECK --interval=5s --timeout=5s --retries=36 --start-period=15s \ + CMD python3 -c "import socket,sys; s=socket.socket(); s.settimeout(4); sys.exit(0) if not s.connect_ex(('127.0.0.1', 8080)) else sys.exit(1)" + #install dependency -> git RUN apt-get update -qqy && \ apt-get install -qqy git wget && \ diff --git a/automation/run-e2e/lib/ci.mjs b/automation/run-e2e/lib/ci.mjs index dfc6293138..d0fe1e097e 100644 --- a/automation/run-e2e/lib/ci.mjs +++ b/automation/run-e2e/lib/ci.mjs @@ -2,6 +2,8 @@ import c from "ansi-colors"; import findFreePort from "find-free-port"; import nodeIp from "ip"; import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import p from "node:path"; import sh from "shelljs"; import parseArgs from "yargs-parser"; import { createDeploymentBundle, prepareImage, startPlaywright, startRuntime } from "./docker-utils.mjs"; @@ -12,20 +14,24 @@ import * as config from "./config.mjs"; const { ls } = sh; +// Path to the docker directory (contains docker-compose.yml + Dockerfiles) +const DOCKER_DIR = fileURLToPath(new URL("../docker", import.meta.url)); +const COMPOSE_FILE = p.join(DOCKER_DIR, "docker-compose.yml"); + export async function ci() { console.log(c.cyan("Run e2e tests in CI environment")); const parseArgsOptions = { string: ["mx-version"], - boolean: ["update-project", "setup-project"], + boolean: ["update-project", "setup-project", "use-compose"], default: { "update-project": true, - "setup-project": true + "setup-project": true, + // Docker Compose is the default; pass --no-use-compose for raw Docker. + "use-compose": true }, configuration: { - // https://github.com/yargs/yargs-parser#boolean-negation "boolean-negation": true, - // https://github.com/yargs/yargs-parser#camel-case-expansion "camel-case-expansion": true } }; @@ -54,22 +60,81 @@ export async function ci() { await updateTestProject(); } + if (options.useCompose) { + await runWithCompose({ mendixVersion, ip, freePort }); + } else { + await runWithDockerRaw({ mendixVersion, ip, freePort }); + } +} + +// --------------------------------------------------------------------------- +// Compose-based orchestration +// Uses "docker compose up --wait" with Docker's built-in healthcheck +// instead of a manual mxbuild → startRuntime → poll loop. +// --------------------------------------------------------------------------- +async function runWithCompose({ mendixVersion, ip, freePort }) { + // Ensure images exist (pulled from cache or built locally) + await prepareImage("mxbuild", mendixVersion); + await prepareImage("mxruntime", mendixVersion); + + const projectFile = ls(config.mprFileGlob).toString(); + if (!projectFile) { + throw new Error(`No .mpr file found matching ${config.mprFileGlob}`); + } + + const mprRelPath = projectFile; // relative to process.cwd() → /source mount + const workspace = process.cwd(); + + const composeEnv = { + ...process.env, + MENDIX_VERSION: mendixVersion, + WORKSPACE: workspace, + MPR_PATH: mprRelPath, + RUNTIME_PORT: String(freePort), + ...(process.env.MODERN_CLIENT ? { MODERN_CLIENT: `--modern-client` } : {}) + }; + + try { + console.log(c.cyan("Starting mxbuild + mxruntime via Docker Compose...")); + console.log(c.gray(` compose file : ${COMPOSE_FILE}`)); + console.log(c.gray(` workspace : ${workspace}`)); + console.log(c.gray(` mpr : ${mprRelPath}`)); + console.log(c.gray(` runtime port : ${freePort}`)); + + // Compose starts mxbuild first (via depends_on), then boots mxruntime. + // Only wait on mxruntime – including mxbuild confuses Compose since its + // clean exit is treated as a failure by --wait. + execSync(`docker compose -f "${COMPOSE_FILE}" up --wait mxruntime`, { stdio: "inherit", env: composeEnv }); + + console.log(c.green("Runtime is healthy. Starting Playwright...")); + startPlaywright(ip, freePort); + } finally { + console.log(c.cyan("Tearing down Docker Compose services...")); + execSync(`docker compose -f "${COMPOSE_FILE}" down --volumes --remove-orphans`, { + stdio: "inherit", + env: composeEnv + }); + } +} + +// --------------------------------------------------------------------------- +// Legacy raw-Docker orchestration (kept as fallback; use --no-use-compose) +// --------------------------------------------------------------------------- +async function runWithDockerRaw({ mendixVersion, ip, freePort }) { let runtimeContainerId; try { const mxbuildImage = await prepareImage("mxbuild", mendixVersion); const mxruntimeImage = await prepareImage("mxruntime", mendixVersion); - // Build testProject via mxbuild const projectFile = ls(config.mprFileGlob).toString(); createDeploymentBundle(mxbuildImage, projectFile); - // Spin up the runtime and run testProject runtimeContainerId = await startRuntime(mxruntimeImage, mendixVersion, ip, freePort); - - // Runs Playwright command line startPlaywright(ip, freePort); } finally { - execSync(`docker rm -f ${runtimeContainerId}`); + if (runtimeContainerId) { + execSync(`docker rm -f ${runtimeContainerId}`); + } } } diff --git a/automation/run-e2e/playwright.config.cjs b/automation/run-e2e/playwright.config.cjs index 1e4d83c2c7..cbafb7f825 100644 --- a/automation/run-e2e/playwright.config.cjs +++ b/automation/run-e2e/playwright.config.cjs @@ -11,8 +11,9 @@ module.exports = defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : 1, + /* Use 4 workers on CI – the runner has multiple cores and each widget's tests + * are independent, so parallel execution cuts per-widget runtime significantly. */ + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ["list"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 482dd613fa..183ad862e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,22 @@ importers: specifier: ^2.5.4 version: 2.5.8 + automation/e2e-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.10.2 + version: 1.27.1(zod@3.25.76) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ~22.14.0 + version: 22.14.1 + typescript: + specifier: '>5.8.0' + version: 5.9.3 + automation/run-e2e: dependencies: ansi-colors: @@ -3991,6 +4007,12 @@ packages: resolution: {integrity: sha512-Tt1oSC7yBwM05j6/SOLagOAJ/NW7XrXKKqUwcuBY++OZO9YyEWF/i72jFSc3DGW4ZAHfc6HHsTIkhayxyy+DsA==} engines: {node: '>=20.0.0'} + '@hono/node-server@1.19.10': + resolution: {integrity: sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4231,6 +4253,16 @@ packages: engines: {node: '>=20'} hasBin: true + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5360,6 +5392,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -5404,6 +5440,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: @@ -5686,6 +5730,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5733,6 +5781,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -6004,6 +6056,14 @@ packages: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -6020,6 +6080,14 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-and-watch@0.1.8: resolution: {integrity: sha512-Prw3k4Za+C/m/OutNtjy1+7Fq+JTiryrFc5JiR0wRrYQ+yPUnsXV8DNPna7plzEcmNbm8x87fqegp9+ogNqKNQ==} engines: {node: '>=10'} @@ -6040,6 +6108,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.2.0: resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} engines: {node: '>=v18'} @@ -6925,6 +6997,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -6948,6 +7028,16 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -7025,6 +7115,10 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-free-port@2.0.0: resolution: {integrity: sha512-J1j8gfEVf5FN4PR5w5wrZZ7NYs2IvqsHcd03cAeQx3Ec/mo+lKceaVNhpsRKoZpZKbId88o8qh+dwUwzBV6WCg==} @@ -7079,10 +7173,18 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -7394,6 +7496,10 @@ packages: hoist-non-react-statics@2.5.5: resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==} + hono@4.12.4: + resolution: {integrity: sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==} + engines: {node: '>=16.9.0'} + html-element-map@1.3.1: resolution: {integrity: sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==} @@ -7414,6 +7520,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -7443,6 +7553,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -7543,9 +7657,17 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ip@2.0.1: resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -7689,6 +7811,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -7999,6 +8124,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -8051,6 +8179,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8341,6 +8472,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -8357,6 +8492,10 @@ packages: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-refs@1.3.0: resolution: {integrity: sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==} peerDependencies: @@ -8444,10 +8583,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -8617,6 +8764,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8876,6 +9027,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8926,6 +9080,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -9288,6 +9446,10 @@ packages: protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -9313,6 +9475,10 @@ packages: peerDependencies: react: '>=18.0.0 <19.0.0' + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -9356,6 +9522,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -9714,6 +9884,10 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rst-selector-parser@2.2.3: resolution: {integrity: sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==} @@ -9805,6 +9979,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -9816,6 +9994,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -10022,6 +10204,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -10440,6 +10626,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -10589,6 +10779,10 @@ packages: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} @@ -10878,6 +11072,11 @@ packages: resolution: {integrity: sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==} engines: {node: '>= 6'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -12132,6 +12331,10 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@hono/node-server@1.19.10(hono@4.12.4)': + dependencies: + hono: 4.12.4 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -12509,7 +12712,7 @@ snapshots: '@types/react-dom': 18.3.7(@types/react@19.2.2) '@types/react-native': 0.72.8(react-native@0.82.0(@babel/core@7.28.4)(@types/react@19.2.2)(react@18.3.1)) '@types/testing-library__jest-dom': 5.14.9 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3) '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.9.3) ansi-colors: 4.1.1 babel-eslint: 10.1.0(eslint@7.32.0) @@ -12522,7 +12725,7 @@ snapshots: enzyme-to-json: 3.6.2(enzyme@3.11.0) eslint: 7.32.0 eslint-config-prettier: 8.10.2(eslint@7.32.0) - eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3) + eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3) eslint-plugin-prettier: 3.4.1(eslint-config-prettier@8.10.2(eslint@7.32.0))(eslint@7.32.0)(prettier@3.5.3) eslint-plugin-promise: 4.3.1 eslint-plugin-react: 7.28.0(eslint@7.32.0) @@ -12588,6 +12791,28 @@ snapshots: - tslib - utf-8-validate + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.10(hono@4.12.4) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.4 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13408,7 +13633,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.9.3) @@ -13416,7 +13641,7 @@ snapshots: '@typescript-eslint/type-utils': 5.62.0(eslint@7.32.0)(typescript@5.9.3) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.3(jiti@2.6.1) + eslint: 7.32.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare-lite: 1.4.0 @@ -13850,6 +14075,11 @@ snapshots: mime-types: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + negotiator: 1.0.0 + acorn-globals@7.0.1: dependencies: acorn: 8.15.0 @@ -13887,6 +14117,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@5.1.0(ajv@8.17.1): dependencies: ajv: 8.17.1 @@ -14220,6 +14454,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -14271,6 +14519,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -14586,6 +14836,10 @@ snapshots: transitivePeerDependencies: - supports-color + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -14603,6 +14857,10 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-and-watch@0.1.8: dependencies: chokidar: 3.6.0 @@ -14628,6 +14886,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig-typescript-loader@6.2.0(@types/node@22.14.1)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 22.14.1 @@ -15409,12 +15672,12 @@ snapshots: eslint: 9.39.3(jiti@2.6.1) globals: 17.3.0 - eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3): + eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3): dependencies: '@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.9.3) eslint: 7.32.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3) transitivePeerDependencies: - supports-color - typescript @@ -15707,6 +15970,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -15743,6 +16012,44 @@ snapshots: exponential-backoff@3.1.3: {} + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.7.0: dependencies: type: 2.7.3 @@ -15824,6 +16131,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-free-port@2.0.0: {} find-up@4.1.0: @@ -15888,8 +16206,12 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} + from2@2.3.0: dependencies: inherits: 2.0.4 @@ -16278,6 +16600,8 @@ snapshots: hoist-non-react-statics@2.5.5: {} + hono@4.12.4: {} + html-element-map@1.3.1: dependencies: array.prototype.filter: 1.0.4 @@ -16311,6 +16635,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -16345,6 +16677,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icss-replace-symbols@1.1.0: {} icss-utils@5.1.0(postcss@8.5.6): @@ -16422,8 +16758,12 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@10.0.1: {} + ip@2.0.1: {} + ipaddr.js@1.9.1: {} + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -16542,6 +16882,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -17095,6 +17437,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.3: {} + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -17163,6 +17507,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-pretty-compact@4.0.0: {} @@ -17474,6 +17820,8 @@ snapshots: mdurl@2.0.0: {} + media-typer@1.1.0: {} + memoize-one@5.2.1: {} memoize-one@6.0.0: {} @@ -17490,6 +17838,8 @@ snapshots: meow@12.1.1: {} + merge-descriptors@2.0.0: {} + merge-refs@1.3.0(@types/react@19.2.2): optionalDependencies: '@types/react': 19.2.2 @@ -17723,10 +18073,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb): dependencies: mime-db: 1.52.0 + mime-types@3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb): + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.5.2: {} @@ -17874,6 +18230,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} next-tick@1.1.0: {} @@ -18134,6 +18492,8 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} path2d@0.2.2: @@ -18167,6 +18527,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -18585,6 +18947,11 @@ snapshots: protocol-buffers-schema@3.6.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + prr@1.0.1: {} psl@1.15.0: @@ -18607,6 +18974,10 @@ snapshots: dependencies: react: 18.3.1 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -18651,6 +19022,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -19215,6 +19593,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rst-selector-parser@2.2.3: dependencies: lodash.flattendeep: 4.4.0 @@ -19313,6 +19701,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serialize-javascript@6.0.2: @@ -19328,6 +19732,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -19539,6 +19952,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -19970,6 +20385,12 @@ snapshots: type-fest@4.41.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + type@2.7.3: {} typed-array-buffer@1.0.3: @@ -20119,6 +20540,8 @@ snapshots: validate-npm-package-name@7.0.2: {} + vary@1.1.2: {} + vlq@0.2.3: {} vlq@1.0.1: {} @@ -20421,6 +20844,10 @@ snapshots: compress-commons: 2.1.1 readable-stream: 3.6.2 + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): dependencies: zod: 3.25.76 From 096320c6d7e9b925c36857512e3744eeef6b6f22 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Tue, 3 Mar 2026 16:41:57 +0100 Subject: [PATCH 2/8] test: dynamic matrix and better test report --- .github/workflows/BuildJobs.yml | 90 +++++++++++++++---- automation/run-e2e/bin/run-e2e-in-chunks.mjs | 29 +++++- .../ctrf-custom-template/custom-summary.hbs | 53 +++++------ 3 files changed, 127 insertions(+), 45 deletions(-) diff --git a/.github/workflows/BuildJobs.yml b/.github/workflows/BuildJobs.yml index dc7c56c8ca..96f94b93b1 100644 --- a/.github/workflows/BuildJobs.yml +++ b/.github/workflows/BuildJobs.yml @@ -140,11 +140,43 @@ jobs: # Limit memory to avoid out of memory issues NODE_OPTIONS: "--max-old-space-size=5120 --max_old_space_size=5120" + e2e-plan: + name: Plan E2E matrix + runs-on: ubuntu-latest + needs: [check-sha] + # Same gate as the e2e job + if: >- + ${{ github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == 'mendix/web-widgets' }} + outputs: + matrix: ${{ steps.compute.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Setup pnpm + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + - name: Setup node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + - name: Install dependencies + run: pnpm install + - name: Compute matrix + id: compute + run: | + matrix=$(node ./automation/run-e2e/bin/run-e2e-in-chunks.mjs --print-matrix --event-name ${{ github.event_name }}) + echo "matrix=$matrix" >> $GITHUB_OUTPUT + e2e: name: Run automated end-to-end tests needs: - check-sha - mxversion + - e2e-plan # Run job only if it's push to main or PR from web-widgets, don't run for fork PRs if: >- ${{ github.event_name == 'push' || @@ -163,11 +195,7 @@ jobs: strategy: # when one test fails, DO NOT cancel the other fail-fast: false - matrix: - # 8 parallel runners halve widget-per-chunk; same total compute, ~2x faster wall-clock. - index: [0, 1, 2, 3, 4, 5, 6, 7] - include: - - chunks: 8 + matrix: ${{ fromJSON(needs.e2e-plan.outputs.matrix) }} steps: - name: Download mxtools cache @@ -214,17 +242,13 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: >- node ./automation/run-e2e/bin/run-e2e-in-chunks.mjs --chunks ${{ matrix.chunks }} --index ${{ matrix.index }} --event-name ${{ github.event_name }} - - name: Check file existence - id: check_files - uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 - with: - files: "./automation/run-e2e/ctrf/*.json" + - name: "Upload CTRF reports" + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: always() - - name: "Generating Github Test Summary" - if: steps.check_files.outputs.files_exists == 'true' - run: | - pnpm --filter run-e2e run report:merge - pnpm dlx github-actions-ctrf custom ./automation/run-e2e/ctrf/merged-report.json ./automation/run-e2e/ctrf-custom-template/custom-summary.hbs + with: + name: ctrf-reports-${{ matrix.index }} + path: ./automation/run-e2e/ctrf/*.json + if-no-files-found: ignore - name: "Fixing files permissions" if: failure() run: | @@ -239,3 +263,39 @@ jobs: ${{ github.workspace }}/packages/**/**/test-results/**/*.png ${{ github.workspace }}/packages/**/**/test-results/**/*.zip if-no-files-found: error + + e2e-report: + name: Publish E2E test summary + runs-on: ubuntu-latest + needs: [e2e] + if: always() + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + - name: Setup pnpm + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + - name: Setup node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + - name: Install dependencies + run: pnpm install + - name: Download CTRF reports from all chunks + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + pattern: ctrf-reports-* + path: ./automation/run-e2e/ctrf + merge-multiple: true + - name: Check file existence + id: check_files + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: "./automation/run-e2e/ctrf/*.json" + - name: Generate test summary + if: steps.check_files.outputs.files_exists == 'true' + run: | + pnpm --filter run-e2e run report:merge + pnpm dlx github-actions-ctrf custom ./automation/run-e2e/ctrf/merged-report.json ./automation/run-e2e/ctrf-custom-template/custom-summary.hbs diff --git a/automation/run-e2e/bin/run-e2e-in-chunks.mjs b/automation/run-e2e/bin/run-e2e-in-chunks.mjs index 236d33dc4c..5e692bf951 100644 --- a/automation/run-e2e/bin/run-e2e-in-chunks.mjs +++ b/automation/run-e2e/bin/run-e2e-in-chunks.mjs @@ -10,6 +10,10 @@ * 2. Sort heaviest-first. * 3. Assign each package to the bin with the lowest current weight. * + * --print-matrix: instead of running tests, prints the GitHub Actions matrix JSON + * to stdout and exits. Used by the e2e-plan CI job to build a dynamic matrix + * sized to the number of packages in scope (PR: changed only; push: all). + * * Playwright sharding: pass --use-playwright-shard to enable Playwright's * native --shard flag within each per-widget run. Requires Playwright ≥ 1.31. */ @@ -21,24 +25,41 @@ import { join } from "node:path"; import parseArgs from "yargs-parser"; import assert from "node:assert/strict"; +const MAX_CHUNKS = 8; + function main() { const parseArgsOptions = { number: ["index", "chunks"], string: ["event-name"], - boolean: ["use-playwright-shard", "debug-chunks"], + boolean: ["use-playwright-shard", "debug-chunks", "print-matrix"], coerce: {}, default: { - chunks: 8, + chunks: MAX_CHUNKS, "event-name": "push", "use-playwright-shard": false, - "debug-chunks": false + "debug-chunks": false, + "print-matrix": false } }; const options = parseArgs(process.argv.slice(2), parseArgsOptions); + const eventName = options.eventName; + + // --print-matrix: output the GitHub Actions matrix JSON and exit. + // Called by the e2e-plan CI job to size the matrix to packages in scope. + if (options.printMatrix) { + const packages = getPackages({ onlyChanged: eventName === "pull_request" }); + const needed = Math.min(Math.max(packages.length, 1), MAX_CHUNKS); + const matrix = { + index: Array.from({ length: needed }, (_, i) => i), + include: [{ chunks: needed }] + }; + process.stdout.write(JSON.stringify(matrix) + "\n"); + return; + } + const index = validateParam(options.index, "index"); const chunks = validateParam(options.chunks, "chunks"); - const eventName = options.eventName; if (index < 0 || index >= chunks) { const message = `Out of range. Received: index - ${index}, chunks - ${chunks}`; diff --git a/automation/run-e2e/ctrf-custom-template/custom-summary.hbs b/automation/run-e2e/ctrf-custom-template/custom-summary.hbs index be024e03ee..2972b2640f 100644 --- a/automation/run-e2e/ctrf-custom-template/custom-summary.hbs +++ b/automation/run-e2e/ctrf-custom-template/custom-summary.hbs @@ -1,40 +1,41 @@ -# Test Summary! +## 🧪 E2E Test Results -| **Tests 📝** | **Passed ✅** | **Failed ❌** | **Skipped ⏭️** | **Pending ⏳** | **Other ❓** | **Flaky 🍂** | **Duration ⏱️** | -| --- | --- | --- | --- | --- | --- | --- | --- | -| {{ctrf.summary.tests}} | {{ctrf.summary.passed}} | {{ctrf.summary.failed}} | {{ctrf.summary.skipped}} | {{ctrf.summary.pending}} | {{ctrf.summary.other}} | {{countFlaky ctrf.tests}} | {{formatDuration ctrf.summary.start ctrf.summary.stop}} | +{{#if (eq ctrf.summary.failed 0)}} +> ✅ **All {{ctrf.summary.tests}} tests passed** — {{formatDuration ctrf.summary.start ctrf.summary.stop}} +{{else}} +> ❌ **{{ctrf.summary.failed}} of {{ctrf.summary.tests}} tests failed** — {{formatDuration ctrf.summary.start ctrf.summary.stop}} +{{/if}} -## Failed tests +| Tests | Passed ✅ | Failed ❌ | Flaky 🔁 | Skipped ⏭️ | Duration ⏱️ | +| :---: | :---: | :---: | :---: | :---: | :---: | +| {{ctrf.summary.tests}} | {{ctrf.summary.passed}} | {{ctrf.summary.failed}} | {{countFlaky ctrf.tests}} | {{ctrf.summary.skipped}} | {{formatDuration ctrf.summary.start ctrf.summary.stop}} | -| **Name** | **Status** | **Failure Message** | +--- + +### ❌ Failed Tests + +| Suite | Test | Error | | --- | --- | --- | -{{#each ctrf.tests}} - {{#if (eq status "failed")}} - | {{name}} | {{status}} ❌ | {{#if message}}{{message}}{{else}}No failure message{{/if}} | - {{/if}} -{{/each}} +{{#each ctrf.tests}}{{#if (eq status "failed")}}| {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} |
Show{{#if message}}{{message}}{{else}}No message{{/if}}
| +{{/if}}{{/each}} -## Flaky tests +
+🔁 Flaky Tests ({{countFlaky ctrf.tests}}) -| **Name** | **Status** | **Failure Message** | +| Suite | Test | Retries | | --- | --- | --- | -{{#each ctrf.tests}} - {{#if (eq status "flaky")}} - | {{name}} | {{status}} 🍂 | {{#if message}}{{message}}{{else}}No failure message{{/if}} | - {{/if}} -{{/each}} +{{#each ctrf.tests}}{{#if flaky}}| {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} | {{retries}} | +{{/if}}{{/each}} -## Detailed Test Results +
- See detailed test results +📋 All Results ({{ctrf.summary.tests}} tests) -
+| | Suite | Test | Duration | +| --- | --- | --- | --- | +{{#each ctrf.tests}}| {{#if (eq status "passed")}}✅{{else}}❌{{/if}} | {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} | {{formatDuration 0 duration}} | +{{/each}} - | **Name** | **Status** | **Failure Message** | - | --- | --- | --- | - {{#each ctrf.tests}} - | {{name}} | {{status}} | {{#if message}}{{message}}{{else}}No failure message{{/if}} | - {{/each}}
From 29745c35474aafdb19c18f1d50846002a7d72e23 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Wed, 4 Mar 2026 12:24:20 +0100 Subject: [PATCH 3/8] test: fixed escaping > in the suite name --- automation/run-e2e/ctrf-custom-template/custom-summary.hbs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/automation/run-e2e/ctrf-custom-template/custom-summary.hbs b/automation/run-e2e/ctrf-custom-template/custom-summary.hbs index 2972b2640f..e156e2d836 100644 --- a/automation/run-e2e/ctrf-custom-template/custom-summary.hbs +++ b/automation/run-e2e/ctrf-custom-template/custom-summary.hbs @@ -16,7 +16,7 @@ | Suite | Test | Error | | --- | --- | --- | -{{#each ctrf.tests}}{{#if (eq status "failed")}}| {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} |
Show{{#if message}}{{message}}{{else}}No message{{/if}}
| +{{#each ctrf.tests}}{{#if (eq status "failed")}}| {{#if suite}}`{{{suite}}}`{{else}}—{{/if}} | {{name}} |
Show{{#if message}}{{message}}{{else}}No message{{/if}}
| {{/if}}{{/each}}
@@ -24,7 +24,7 @@ | Suite | Test | Retries | | --- | --- | --- | -{{#each ctrf.tests}}{{#if flaky}}| {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} | {{retries}} | +{{#each ctrf.tests}}{{#if flaky}}| {{#if suite}}`{{{suite}}}`{{else}}—{{/if}} | {{name}} | {{retries}} | {{/if}}{{/each}}
@@ -34,7 +34,7 @@ | | Suite | Test | Duration | | --- | --- | --- | --- | -{{#each ctrf.tests}}| {{#if (eq status "passed")}}✅{{else}}❌{{/if}} | {{#if suite}}`{{suite}}`{{else}}—{{/if}} | {{name}} | {{formatDuration 0 duration}} | +{{#each ctrf.tests}}| {{#if (eq status "passed")}}✅{{else}}❌{{/if}} | {{#if suite}}`{{{suite}}}`{{else}}—{{/if}} | {{name}} | {{formatDuration 0 duration}} | {{/each}} From f6726e282d0b58e9fc17dbb1402e3f9e909d9388 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Wed, 4 Mar 2026 12:40:30 +0100 Subject: [PATCH 4/8] test: update actions in the BuildJobs workflow --- .github/workflows/BuildJobs.yml | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/BuildJobs.yml b/.github/workflows/BuildJobs.yml index 96f94b93b1..abc9e9c5fb 100644 --- a/.github/workflows/BuildJobs.yml +++ b/.github/workflows/BuildJobs.yml @@ -23,8 +23,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@38608ef4fb69adae7f1eac6eeb88e67b7d083bfd # v3.0.16 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1 check: name: Run code quality check @@ -33,20 +33,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" - name: Get sha of main run: echo "main_sha=$(git rev-parse origin/main)" >> $GITHUB_ENV - name: Restore Turbo Cache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: node_modules/.cache/turbo # NOTE: We create new cache record for every new commit on main @@ -84,7 +84,7 @@ jobs: mx_tools_cache_key: mx-tools-cache:${{ steps.readfile.outputs.mxversion }} steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: Set job outputs @@ -109,20 +109,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" - name: Get sha of main run: echo "main_sha=$(git rev-parse origin/main)" >> ${{ runner.os == 'Windows' && '$env:GITHUB_ENV' || '$GITHUB_ENV' }} - name: Restore Turbo Cache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: node_modules/.cache/turbo # NOTE: build & release tasks should have their own cache @@ -153,13 +153,13 @@ jobs: matrix: ${{ steps.compute.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" @@ -199,7 +199,7 @@ jobs: steps: - name: Download mxtools cache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 id: cache with: path: | @@ -214,20 +214,20 @@ jobs: docker load --input ${{ env.MXRUNTIME_PATH }} docker image ls -a - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" - name: Get sha of main run: echo "main_sha=$(git rev-parse origin/main)" >> ${{ runner.os == 'Windows' && '$env:GITHUB_ENV' || '$GITHUB_ENV' }} - name: Restore Turbo Cache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: node_modules/.cache/turbo key: turbo-cache-${{ runner.os }}-e2e-chunk-${{ matrix.index }}-${{ env.main_sha }} @@ -243,7 +243,7 @@ jobs: run: >- node ./automation/run-e2e/bin/run-e2e-in-chunks.mjs --chunks ${{ matrix.chunks }} --index ${{ matrix.index }} --event-name ${{ github.event_name }} - name: "Upload CTRF reports" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: always() with: name: ctrf-reports-${{ matrix.index }} @@ -255,7 +255,7 @@ jobs: sudo find ${{ github.workspace }}/packages/* -type d -exec chmod 755 {} \; sudo find ${{ github.workspace }}/packages/* -type f -exec chmod 644 {} \; - name: "Archive test screenshot diff results" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: test-screenshot-results-${{ matrix.index }} @@ -271,20 +271,20 @@ jobs: if: always() steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: Setup pnpm - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" - name: Install dependencies run: pnpm install - name: Download CTRF reports from all chunks - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: ctrf-reports-* path: ./automation/run-e2e/ctrf From ab5350de8efae00cffea2118129e123a112923d3 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Wed, 4 Mar 2026 13:02:30 +0100 Subject: [PATCH 5/8] test: updated playwright and other outdated dependencies --- automation/run-e2e/package.json | 18 ++-- pnpm-lock.yaml | 166 +++++++++++++++++++++++--------- 2 files changed, 129 insertions(+), 55 deletions(-) diff --git a/automation/run-e2e/package.json b/automation/run-e2e/package.json index 0891c8e505..1dac43dbee 100644 --- a/automation/run-e2e/package.json +++ b/automation/run-e2e/package.json @@ -23,18 +23,18 @@ "enquirer": "^2.4.1", "find-free-port": "^2.0.0", "ip": "^2.0.1", - "node-fetch": "^2.7.0", - "shelljs": "^0.8.5", - "yargs-parser": "^21.1.1" + "node-fetch": "^3.3.2", + "shelljs": "^0.10.0", + "yargs-parser": "^22.0.0" }, "devDependencies": { - "@axe-core/playwright": "^4.10.1", - "@eslint/js": "^9.32.0", + "@axe-core/playwright": "^4.11.1", + "@eslint/js": "^10.0.1", "@mendix/prettier-config-web-widgets": "workspace:*", - "@playwright/test": "^1.51.1", + "@playwright/test": "^1.58.2", "@types/node": "*", - "eslint-plugin-playwright": "^2.2.0", - "globals": "^17.3.0", - "playwright-ctrf-json-reporter": "^0.0.20" + "eslint-plugin-playwright": "^2.9.0", + "globals": "^17.4.0", + "playwright-ctrf-json-reporter": "^0.0.27" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183ad862e7..f0d727f8ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,39 +96,39 @@ importers: specifier: ^2.0.1 version: 2.0.1 node-fetch: - specifier: ^2.7.0 - version: 2.7.0 + specifier: ^3.3.2 + version: 3.3.2 shelljs: - specifier: ^0.8.5 - version: 0.8.5 + specifier: ^0.10.0 + version: 0.10.0 yargs-parser: - specifier: ^21.1.1 - version: 21.1.1 + specifier: ^22.0.0 + version: 22.0.0 devDependencies: '@axe-core/playwright': - specifier: ^4.10.1 - version: 4.10.2(playwright-core@1.56.0) + specifier: ^4.11.1 + version: 4.11.1(playwright-core@1.58.2) '@eslint/js': - specifier: ^9.32.0 - version: 9.37.0 + specifier: ^10.0.1 + version: 10.0.1(eslint@9.39.3(jiti@2.6.1)) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../packages/shared/prettier-config-web-widgets '@playwright/test': - specifier: ^1.51.1 - version: 1.56.0 + specifier: ^1.58.2 + version: 1.58.2 '@types/node': specifier: ~22.14.0 version: 22.14.1 eslint-plugin-playwright: - specifier: ^2.2.0 - version: 2.2.2(eslint@9.39.3(jiti@2.6.1)) + specifier: ^2.9.0 + version: 2.9.0(eslint@9.39.3(jiti@2.6.1)) globals: - specifier: ^17.3.0 - version: 17.3.0 + specifier: ^17.4.0 + version: 17.4.0 playwright-ctrf-json-reporter: - specifier: ^0.0.20 - version: 0.0.20 + specifier: ^0.0.27 + version: 0.0.27 automation/scripts: dependencies: @@ -3068,8 +3068,8 @@ packages: '@altano/repository-tools@2.0.1': resolution: {integrity: sha512-YE/52CkFtb+YtHPgbWPai7oo5N9AKnMuP5LM+i2AG7G1H2jdYBCO1iDnkDE3dZ3C1MIgckaF+d5PNRulgt0bdw==} - '@axe-core/playwright@4.10.2': - resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==} + '@axe-core/playwright@4.11.1': + resolution: {integrity: sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==} peerDependencies: playwright-core: '>= 1.0.0' @@ -3963,6 +3963,15 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + '@eslint/js@9.37.0': resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4374,8 +4383,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.56.0': - resolution: {integrity: sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true @@ -5633,8 +5642,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} babel-eslint@10.1.0: @@ -6324,6 +6333,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -6810,9 +6823,9 @@ packages: eslint: '>=8.0.0' jsonc-eslint-parser: '>=2.0.0' - eslint-plugin-playwright@2.2.2: - resolution: {integrity: sha512-j0jKpndIPOXRRP9uMkwb9l/nSmModOU3452nrFdgFJoEv/435J1onk8+aITzjDW8DfypxgmVaDMdmVIa6F7I0w==} - engines: {node: '>=16.6.0'} + eslint-plugin-playwright@2.9.0: + resolution: {integrity: sha512-k3xrG6YzrallWNFMoGUjMNeu3SFFKXN79KJQBD2PkM4PasJegqV2Up+mPY5od2UmPKQGT+MeIhCmWH8r5eYuQQ==} + engines: {node: '>=16.9.0'} peerDependencies: eslint: '>=8.40.0' @@ -7095,6 +7108,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -7173,6 +7190,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -7350,6 +7371,10 @@ packages: resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} engines: {node: '>=18'} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -8781,6 +8806,11 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -8790,6 +8820,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -9088,16 +9122,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - playwright-core@1.56.0: - resolution: {integrity: sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright-ctrf-json-reporter@0.0.20: - resolution: {integrity: sha512-pv8ZZ1PBgztgQ1iShB8NqcFigTWGeZZ8zowld3wLlaeRF3bo4O9QrB5qlb2W7HcgppVWbM8SDvUszRLiLi923A==} + playwright-ctrf-json-reporter@0.0.27: + resolution: {integrity: sha512-FZ8KadoHJc7xhf5XM0R9F8XBsTSm4vywa5/fhmeo2nZhN31UmapYwRfxaBsGk6AbsvGmft5G+MVmkBjTJZic/Q==} - playwright@1.56.0: - resolution: {integrity: sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -10038,6 +10072,10 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shelljs@0.10.0: + resolution: {integrity: sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==} + engines: {node: '>=18'} + shelljs@0.8.5: resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} engines: {node: '>=4'} @@ -10821,6 +10859,10 @@ packages: weak-map@1.0.8: resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webgl-context@2.2.0: resolution: {integrity: sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==} @@ -11092,10 +11134,10 @@ snapshots: '@altano/repository-tools@2.0.1': {} - '@axe-core/playwright@4.10.2(playwright-core@1.56.0)': + '@axe-core/playwright@4.11.1(playwright-core@1.58.2)': dependencies: - axe-core: 4.10.3 - playwright-core: 1.56.0 + axe-core: 4.11.1 + playwright-core: 1.58.2 '@babel/code-frame@7.12.11': dependencies: @@ -12282,6 +12324,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@10.0.1(eslint@9.39.3(jiti@2.6.1))': + optionalDependencies: + eslint: 9.39.3(jiti@2.6.1) + '@eslint/js@9.37.0': {} '@eslint/js@9.39.3': {} @@ -12893,9 +12939,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.56.0': + '@playwright/test@1.58.2': dependencies: - playwright: 1.56.0 + playwright: 1.58.2 '@plotly/d3-sankey-circular@0.33.1': dependencies: @@ -14324,7 +14370,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.10.3: {} + axe-core@4.11.1: {} babel-eslint@10.1.0(eslint@7.32.0): dependencies: @@ -15130,6 +15176,8 @@ snapshots: dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} + data-urls@3.0.2: dependencies: abab: 2.0.6 @@ -15670,7 +15718,7 @@ snapshots: eslint-plugin-cypress@5.4.0(eslint@9.39.3(jiti@2.6.1)): dependencies: eslint: 9.39.3(jiti@2.6.1) - globals: 17.3.0 + globals: 17.4.0 eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3): dependencies: @@ -15710,10 +15758,10 @@ snapshots: transitivePeerDependencies: - '@types/estree' - eslint-plugin-playwright@2.2.2(eslint@9.39.3(jiti@2.6.1)): + eslint-plugin-playwright@2.9.0(eslint@9.39.3(jiti@2.6.1)): dependencies: eslint: 9.39.3(jiti@2.6.1) - globals: 13.24.0 + globals: 17.4.0 eslint-plugin-prettier@3.4.1(eslint-config-prettier@8.10.2(eslint@7.32.0))(eslint@7.32.0)(prettier@3.5.3): dependencies: @@ -16103,6 +16151,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -16206,6 +16259,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -16410,6 +16467,8 @@ snapshots: globals@17.3.0: {} + globals@17.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -18244,10 +18303,18 @@ snapshots: node-addon-api@7.1.1: optional: true + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-int64@0.4.0: {} node-releases@2.0.23: {} @@ -18533,13 +18600,13 @@ snapshots: dependencies: find-up: 4.1.0 - playwright-core@1.56.0: {} + playwright-core@1.58.2: {} - playwright-ctrf-json-reporter@0.0.20: {} + playwright-ctrf-json-reporter@0.0.27: {} - playwright@1.56.0: + playwright@1.58.2: dependencies: - playwright-core: 1.56.0 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -19783,6 +19850,11 @@ snapshots: shell-quote@1.8.3: {} + shelljs@0.10.0: + dependencies: + execa: 5.1.1 + fast-glob: 3.3.3 + shelljs@0.8.5: dependencies: glob: 7.2.3 @@ -20585,6 +20657,8 @@ snapshots: weak-map@1.0.8: {} + web-streams-polyfill@3.3.3: {} + webgl-context@2.2.0: dependencies: get-canvas-context: 1.0.2 From c267df7b379cd05ce62e1a4de066858d9e0fa4b2 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Wed, 4 Mar 2026 14:39:27 +0100 Subject: [PATCH 6/8] test: fix lint errors --- automation/run-e2e/lib/ci.mjs | 2 +- automation/run-e2e/lib/setup-test-project.mjs | 4 ++-- automation/run-e2e/lib/update-test-project.mjs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/automation/run-e2e/lib/ci.mjs b/automation/run-e2e/lib/ci.mjs index d0fe1e097e..4d7bd0b716 100644 --- a/automation/run-e2e/lib/ci.mjs +++ b/automation/run-e2e/lib/ci.mjs @@ -49,7 +49,7 @@ export async function ci() { try { execSync("docker info"); } catch (e) { - throw new Error("To run e2e test locally, make sure docker is running. Exiting now..."); + throw new Error("To run e2e test locally, make sure docker is running. Exiting now...", { cause: e }); } if (options.setupProject) { diff --git a/automation/run-e2e/lib/setup-test-project.mjs b/automation/run-e2e/lib/setup-test-project.mjs index e2a8e8aecf..7fa8e94fb1 100644 --- a/automation/run-e2e/lib/setup-test-project.mjs +++ b/automation/run-e2e/lib/setup-test-project.mjs @@ -36,7 +36,7 @@ export async function setupTestProject() { } } catch (e) { console.error(e); - throw new Error(`Failed to unzip the test project into ${config.testProjectDir}`); + throw new Error(`Failed to unzip the test project into ${config.testProjectDir}`, { cause: e }); } } @@ -56,6 +56,6 @@ async function downloadTestProject(repository, branch) { return downloadedArchivePath; } catch (e) { rm("-f", downloadedArchivePath); - throw new Error("Cannot find test project in GitHub repository. Try again later."); + throw new Error("Cannot find test project in GitHub repository. Try again later.", { cause: e }); } } diff --git a/automation/run-e2e/lib/update-test-project.mjs b/automation/run-e2e/lib/update-test-project.mjs index 0f86ef0ccc..af9b947ee4 100644 --- a/automation/run-e2e/lib/update-test-project.mjs +++ b/automation/run-e2e/lib/update-test-project.mjs @@ -24,7 +24,7 @@ async function downloadAndExtract(url, downloadPath, extractPath) { await streamPipe((await fetchWithReport(url)).body, createWriteStream(downloadPath)); crossZip.unzipSync(downloadPath, extractPath); } catch (e) { - throw new Error(`Unable to download and extract from ${url}`); + throw new Error(`Unable to download and extract from ${url}`, { cause: e }); } finally { rm("-f", downloadPath); } From e304e73dc9cc457920872d7d30a3348236b229ad Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Thu, 5 Mar 2026 11:50:43 +0100 Subject: [PATCH 7/8] test(rich-text): update screenshot baseline --- .../richTextModal-chromium-linux.png | Bin 37500 -> 35462 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/richTextModal-chromium-linux.png b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/richTextModal-chromium-linux.png index 7366cd2c9ad6e91af587a14646fc5ae48d838c71..a3822f190395a208ef8bd73caec8c3ba57fd3fee 100644 GIT binary patch literal 35462 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 literal 37500 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 From 2a524e4e7a77be9a1e002d9fba515c4b5c507c4b Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Thu, 5 Mar 2026 14:07:49 +0100 Subject: [PATCH 8/8] test(video-player): change wait to domcontent loaded --- .../video-player-web/e2e/VideoPlayer.spec.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/pluggableWidgets/video-player-web/e2e/VideoPlayer.spec.js b/packages/pluggableWidgets/video-player-web/e2e/VideoPlayer.spec.js index 7f76e9c22e..87adec0e4a 100644 --- a/packages/pluggableWidgets/video-player-web/e2e/VideoPlayer.spec.js +++ b/packages/pluggableWidgets/video-player-web/e2e/VideoPlayer.spec.js @@ -1,5 +1,10 @@ import { test, expect } from "@playwright/test"; +async function waitForMendixReady(page) { + await page.waitForLoadState("domcontentloaded"); + await page.waitForFunction(() => !!window.mx?.session); +} + test.afterEach("Cleanup session", async ({ page }) => { // Because the test isolation that will open a new session for every test executed, and that exceeds Mendix's license limit of 5 sessions, so we need to force logout after each test. await page.evaluate(() => window.mx.session.logout()); @@ -8,7 +13,7 @@ test.afterEach("Cleanup session", async ({ page }) => { test.describe("Video Player", () => { test.beforeEach(async ({ page }) => { await page.goto("/p/grid"); - await page.waitForLoadState("networkidle"); + await waitForMendixReady(page); }); test("renders youtube video", async ({ page }) => { @@ -42,7 +47,7 @@ test.describe("Video Player", () => { test.describe("Tab page", () => { test.beforeEach(async ({ page }) => { await page.goto("/p/tabs"); - await page.waitForLoadState("networkidle"); + await waitForMendixReady(page); }); test("renders youtube video", async ({ page }) => { @@ -97,7 +102,7 @@ test.describe("Tab page", () => { test.describe("Error page", () => { test.beforeEach(async ({ page }) => { await page.goto("/p/errors"); - await page.waitForLoadState("networkidle"); + await waitForMendixReady(page); }); test("renders no content div", async ({ page }) => { @@ -111,7 +116,7 @@ test.describe("Error page", () => { test.describe("External video", () => { test.beforeEach(async ({ page }) => { await page.goto("/p/external"); - await page.waitForLoadState("networkidle"); + await waitForMendixReady(page); }); test("renders a poster", async ({ page }) => { @@ -121,6 +126,7 @@ test.describe("External video", () => { test.describe("Video aspect ratio", () => { test.beforeEach(async ({ page }) => { await page.goto("/p/aspectRatio"); + await waitForMendixReady(page); }); test("renders video aspect ratio correctly", async ({ page }) => {