diff --git a/.github/actions/archive-test-results/action.yaml b/.github/actions/archive-test-results/action.yaml index 22f396355..4c4146091 100644 --- a/.github/actions/archive-test-results/action.yaml +++ b/.github/actions/archive-test-results/action.yaml @@ -30,11 +30,46 @@ runs: name: ${{ inputs.platform }}-screenshots-${{ inputs.test-type }} path: ${{ inputs.workspace-path }}/maestro/images/actual/${{ inputs.platform }}/**/*.png if-no-files-found: ignore - + + - name: Collect screenshot diffs + # On a failed assertScreenshot, Maestro writes the visual diff (the most useful artifact + # for triaging a mismatch) next to the baseline — but under a *doubled* path, e.g. + # maestro/images/expected/ios/maestro/images/expected/ios/_diff.png, because it + # resolves the diff name against the baseline's own relative path. That nested location + # was never matched by the screenshots glob above, so diffs were silently lost. Gather + # any *_diff.png from wherever it landed into one flat dir for upload. + if: always() + shell: bash + run: | + dest="${{ inputs.workspace-path }}/maestro/images/diff/${{ inputs.platform }}" + mkdir -p "$dest" + find "${{ inputs.workspace-path }}/maestro/images" -name '*_diff.png' \ + -not -path "$dest/*" -exec cp -f {} "$dest/" \; 2>/dev/null || true + ls -1 "$dest" 2>/dev/null || true + + - name: Archive screenshot diffs + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 + if: always() + with: + name: ${{ inputs.platform }}-diffs-${{ inputs.test-type }} + path: ${{ inputs.workspace-path }}/maestro/images/diff/${{ inputs.platform }}/*.png + if-no-files-found: ignore + - name: Archive artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 if: always() with: name: ${{ inputs.platform }}-artifacts-${{ inputs.test-type }} path: packages/pluggableWidgets/**/artifacts/ + if-no-files-found: ignore + + # Per-flow videos are recorded for every flow but kept only for FAILED flows (passing + # clips are deleted in helpers.sh to save storage), so on a fully-green shard this dir is + # empty and the upload is a no-op (if-no-files-found: ignore). + - name: Archive failure videos + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 + if: always() + with: + name: ${{ inputs.platform }}-videos-${{ inputs.test-type }} + path: maestro/videos/*.mp4 if-no-files-found: ignore \ No newline at end of file diff --git a/.github/actions/create-native-bundle/action.yml b/.github/actions/create-native-bundle/action.yml deleted file mode 100644 index 0b6fa94ea..000000000 --- a/.github/actions/create-native-bundle/action.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Create native bundle" -description: "Create a native bundle for android / iOS" -inputs: - platform: - description: "Target platform (android or ios)" - required: true - mda-file: - description: "Path to the deployment package" - required: true -runs: - using: composite - steps: - - name: "Make sure curl is installed" - run: | - apt update && apt upgrade -y - apt install curl -y - shell: bash - - name: "Download test project" - run: curl -L -o project.zip https://github.com/mendix/Native-Mobile-Resources/archive/refs/heads/main.zip - shell: bash - - name: "Extract test project" - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0 - with: - args: unzip -qq project.zip - - name: "Extract deployment package" - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0 - with: - args: unzip -qq ${{ inputs.mda-file }} -d Native-Mobile-Resources-main/deployment - - name: "Create bundle for ${{ inputs.platform }}" - run: | - mkdir -p ${{ inputs.platform }}/assets Native-Mobile-Resources-main/.mendix-cache/modules - cd Native-Mobile-Resources-main/deployment/native && \ - /tmp/mxbuild/modeler/tools/node/linux-x64/node \ - /tmp/mxbuild/modeler/tools/node/node_modules/@react-native-community/cli/build/bin.js \ - bundle --verbose --platform ${{ inputs.platform }} --dev false \ - --config "$PWD/metro.config.js" \ - --bundle-output $GITHUB_WORKSPACE/${{ inputs.platform }}/index.${{ inputs.platform }}.bundle \ - --assets-dest $GITHUB_WORKSPACE/${{ inputs.platform }}/assets/ \ - --entry-file ./index.js - shell: bash - env: - NODE_OPTIONS: --max_old_space_size=6144 - - name: "Upload bundle for ${{ inputs.platform }}" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: ${{ inputs.platform }}-bundle - path: ${{ inputs.platform }} diff --git a/.github/actions/setup-maestro/action.yaml b/.github/actions/setup-maestro/action.yaml index 237100eda..8ced00390 100644 --- a/.github/actions/setup-maestro/action.yaml +++ b/.github/actions/setup-maestro/action.yaml @@ -1,21 +1,30 @@ name: "Setup Maestro" -description: "Install and cache Maestro" +description: "Install and cache a pinned version of Maestro" +inputs: + version: + description: "Maestro CLI version to install (release tag is cli-)" + required: false + default: "2.6.1" runs: using: "composite" steps: - name: "Cache Maestro" uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: - path: $HOME/.local/bin/maestro - key: maestro-${{ runner.os }}-v1 - + # Use ~ rather than $HOME: actions/cache does not expand environment variables. + path: ~/.local/bin/maestro + key: maestro-${{ runner.os }}-${{ inputs.version }} + - name: "Install Maestro" shell: bash + env: + MAESTRO_VERSION: ${{ inputs.version }} run: | + set -euo pipefail if [ ! -f "$HOME/.local/bin/maestro/bin/maestro" ]; then - mkdir -p $HOME/.local/bin - curl -L "https://github.com/mobile-dev-inc/maestro/releases/latest/download/maestro.zip" -o maestro.zip - unzip maestro.zip -d $HOME/.local/bin - chmod +x $HOME/.local/bin/maestro/bin/maestro + mkdir -p "$HOME/.local/bin" + curl -fL "https://github.com/mobile-dev-inc/maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" -o maestro.zip + unzip -q maestro.zip -d "$HOME/.local/bin" + chmod +x "$HOME/.local/bin/maestro/bin/maestro" fi - echo "$HOME/.local/bin" >> $GITHUB_PATH \ No newline at end of file + echo "$HOME/.local/bin" >> "$GITHUB_PATH" diff --git a/.github/actions/setup-python/action.yaml b/.github/actions/setup-python/action.yaml deleted file mode 100644 index a7244d46b..000000000 --- a/.github/actions/setup-python/action.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Setup Python Common" -description: "Setup Python and install dependencies" -runs: - using: "composite" - steps: - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - run: pip install PyYAML httplib2 - shell: bash \ No newline at end of file diff --git a/.github/actions/setup-tools/action.yaml b/.github/actions/setup-tools/action.yaml index 8ab61a4fd..92604b7c6 100644 --- a/.github/actions/setup-tools/action.yaml +++ b/.github/actions/setup-tools/action.yaml @@ -1,9 +1,5 @@ name: "Setup Tools" description: "Common setup for widget and js test jobs" -inputs: - mendix_version: - description: "Mendix version" - required: true outputs: java-path: description: "Path to the installed Java" @@ -11,12 +7,12 @@ outputs: runs: using: "composite" steps: - - name: "Setup Python and dependencies" - uses: ./.github/actions/setup-python + # Test jobs need only Java + Maestro: + # - the portable app package bundles the runtime, so there's no CDN runtime download and no + # m2ee tooling to set up (and nothing here needs Python). + # - Maestro is a standalone binary, so there's no Node/pnpm install — which would also run the + # root `prepare` lifecycle the test jobs don't need (a known ENOTEMPTY flake source). - - name: "Setup Node and dependencies" - uses: ./.github/actions/setup-node-with-cache - - name: "Setup Java 21" id: setup-java uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 #v5.2.0 @@ -24,18 +20,6 @@ runs: distribution: "temurin" java-version: "21" - - name: "Cache Mendix runtime" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 #v5 - with: - path: tmp/runtime.tar.gz - key: mendix-runtime-${{ inputs.mendix_version }} - - - name: "Cache m2ee-tools" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 #v5 - with: - path: tmp/m2ee - key: m2ee-tools-v1 - - name: "Install Maestro" uses: ./.github/actions/setup-maestro diff --git a/.github/actions/start-mendix-runtime/action.yaml b/.github/actions/start-mendix-runtime/action.yaml new file mode 100644 index 000000000..d047ff4e9 --- /dev/null +++ b/.github/actions/start-mendix-runtime/action.yaml @@ -0,0 +1,81 @@ +name: "Start Mendix runtime (portable app)" +description: "Extract the portable app package, start it in the background, and wait until it actually serves requests" +inputs: + app-package: + description: "Path to the portable app package zip produced by 'mxbuild --target=portable-app-package'" + required: false + default: "app.zip" + java-home: + description: "JAVA_HOME to run the runtime with (defaults to the JAVA_HOME already in the environment)" + required: false + default: "" + admin-pass: + description: "Value for M2EE_ADMIN_PASS (secures the runtime admin port)" + required: false + default: "Password1!" + runtime-url: + description: "URL polled until the runtime responds, confirming readiness" + required: false + default: "http://localhost:8080/" + timeout-seconds: + description: "How long to wait for the runtime to become ready before failing" + required: false + default: "300" + session-timeout-ms: + description: > + Server-side session idle timeout (ms). The runtime uses a trial license capped at + 6 concurrent named users, and every Maestro flow does `launchApp clearState: true`, + which starts a FRESH server session while the previous one lingers for the full + timeout. With Mendix's 10-min default, a widget with >5 flows piles past 6 sessions + and the next login fails with "Maximum number of sessions exceeded". A short idle + timeout lets each flow's session expire before the next ones accumulate. Active + sessions aren't affected — client requests keep resetting the idle timer mid-flow. + required: false + default: "60000" +runs: + using: "composite" + steps: + - name: "Start runtime and wait until ready" + shell: bash + env: + APP_PACKAGE: ${{ inputs.app-package }} + INPUT_JAVA_HOME: ${{ inputs.java-home }} + M2EE_ADMIN_PASS: ${{ inputs.admin-pass }} + RUNTIME_URL: ${{ inputs.runtime-url }} + TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }} + # Read by the portable app's HOCON config (etc/variables.conf: ${?RUNTIME_PARAMS_*}), + # inherited by the JVM started below. Caps session pile-up under the 6-user trial + # license — see the session-timeout-ms input for the full rationale. + RUNTIME_PARAMS_SESSIONTIMEOUT: ${{ inputs.session-timeout-ms }} + # Reap expired sessions promptly (default cluster cleanup runs only every 5 min, which + # would hold license slots long after the sessions go idle). + RUNTIME_PARAMS_CLUSTERMANAGERACTIONINTERVAL: "30000" + run: | + set -euo pipefail + + # Allow an explicit JAVA_HOME override; otherwise rely on the one set up earlier. + if [ -n "$INPUT_JAVA_HOME" ]; then + export JAVA_HOME="$INPUT_JAVA_HOME" + fi + echo "Using JAVA_HOME=${JAVA_HOME:-}" + + rm -rf runtime-app + mkdir -p runtime-app log + unzip -qq "$APP_PACKAGE" -d runtime-app + chmod +x runtime-app/bin/start + + # Start the bundled runtime in the background; it must outlive this step. + (cd runtime-app && nohup ./bin/start etc/Default > "$GITHUB_WORKSPACE/log/runtime.log" 2>&1 &) + + echo "Waiting up to ${TIMEOUT_SECONDS}s for the runtime at ${RUNTIME_URL} ..." + deadline=$(( SECONDS + TIMEOUT_SECONDS )) + until curl -fsS -o /dev/null "$RUNTIME_URL"; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "::error::Mendix runtime did not become ready within ${TIMEOUT_SECONDS}s" + echo "----- last 100 lines of runtime.log -----" + tail -n 100 "$GITHUB_WORKSPACE/log/runtime.log" 2>/dev/null || echo "(no log)" + exit 1 + fi + sleep 3 + done + echo "Mendix runtime is ready and serving at ${RUNTIME_URL}" diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 04ec4206a..c7934dbc9 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -4,40 +4,74 @@ This directory contains helper scripts used by the CI/CD pipeline. ## determine-widget-scope.sh -Determines which widgets and JS actions should be built and tested based on changed files or manual input. +Determines which widgets and JS actions should be built and tested, based on the +files changed in a PR or on manual `workflow_dispatch` input. ### Usage ```bash -./determine-widget-scope.sh +./determine-widget-scope.sh ``` +For a `pull_request`, `` is the PR base SHA +(`github.event.pull_request.base.sha`). The script diffs against the **merge-base** of +that and ``, so every commit in the PR is considered (not just the +latest). This needs full git history — the `scope` job checks out with `fetch-depth: 0`. +On `workflow_dispatch`, `` drives the scope and the commit args are +ignored. + ### Outputs -| Output | Description | -| -------------------- | ---------------------------------------------------- | -| `scope` | pnpm filter scope for building | -| `widgets` | JSON array of widgets to BUILD | -| `widgets_to_test` | JSON array of widgets to TEST (used for test matrix) | -| `js_actions_changed` | Boolean flag indicating if JS actions changed | +| Output | Description | +| -------------------- | -------------------------------------------------------------------------------- | +| `scope` | pnpm filter scope (used by the unit-test step) | +| `widgets` | JSON array of widgets to BUILD | +| `widgets_to_test` | JSON array of widgets to TEST (the test matrix) | +| `js_actions_changed` | Boolean — whether JS actions changed | +| `full_build` | Boolean — true when every widget is (re)built; gates the dist cache restore/save | + +### Build vs. test scope + +`widgets_to_test` and `widgets` are deliberately **separate**: + +- A change anywhere under a widget folder marks it for **testing**. +- Only a **build-affecting** change (anything outside the widget's `e2e/` folder, e.g. + `src/**` or `package.json`) marks it for **building**. A change confined to `e2e/` + (Maestro flows / screenshots) changes only the test, so the widget is tested against + the test project's baseline `.mpk` instead of being rebuilt. + +So `widgets` can be a strict subset of `widgets_to_test` (or empty while +`widgets_to_test` is non-empty, for an e2e-only PR). ### Pipeline Behavior -| Scenario | Widgets Built | Widgets Tested | JS Actions Built | JS Actions Tested | -| ---------------------- | --------------- | --------------- | ---------------- | ----------------- | -| Only JS actions change | All | None | Yes | Yes | -| Only widgets change | Changed only | Changed only | No | No | -| Both change | Changed widgets | Changed widgets | Yes | Yes | -| Full run (`*-native`) | All | All | Yes | Yes | -| Manual: `js-actions` | All | None | Yes | Yes | +| Scenario | Widgets Built | Widgets Tested | JS Actions Built | JS Actions Tested | +| ------------------------------- | ---------------------- | --------------- | ---------------- | ----------------- | +| Only JS actions change | All | None | Yes | Yes | +| Only widget `src`/deps change | Changed only | Changed only | No | No | +| Only widget `e2e/` changes | None (use baseline) | Changed only | No | No | +| Both widgets and JS actions | Build-affected widgets | Changed widgets | Yes | Yes | +| Full run (`*-native` / default) | All | All | Yes | Yes | +| Manual: `js-actions` | All | None | Yes | Yes | ### Why build all widgets when only JS actions change? -The JS action tests run against a full test project that requires all widgets to be present. Without building all widgets, the test project would be incomplete and tests would fail. +The JS action tests run against a full test project that requires all widgets to be +present. Without building all widgets, the test project would be incomplete and tests +would fail. ## Other Scripts -- **determine-nt-version.py** - Determines the Native Template version based on Mendix version -- **mxbuild.Dockerfile** - Docker image for mxbuild -- **setup-runtime.sh** - Sets up the Mendix runtime -- **start-runtime-with-verification.sh** - Starts the runtime with health verification +- **determine-nt-version.mjs** — resolves the Native Template release/branch from the + Mendix version. Node ESM, no external deps (uses global `fetch` + a small version + comparator); run with the runner's preinstalled `node`. Fails loudly rather than + silently pinning `master`. +- **mxbuild.Dockerfile** — Docker image for mxbuild. + +> The Mendix runtime for the test jobs is produced as a **portable app package** +> (`mxbuild --target=portable-app-package`) in the `project` job and started by the +> `start-mendix-runtime` composite action, which waits for a real readiness probe. +> That runtime runs under a 6-user trial license, so the action shortens the server +> session idle timeout (`RUNTIME_PARAMS_SESSIONTIMEOUT`) — each Maestro flow opens a +> fresh session via `launchApp clearState`, and a short timeout lets them expire +> between flows instead of piling up past the cap. diff --git a/.github/scripts/determine-nt-version.mjs b/.github/scripts/determine-nt-version.mjs new file mode 100644 index 000000000..4024b18a7 --- /dev/null +++ b/.github/scripts/determine-nt-version.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +// Resolve which Native Template release/branch to build against, given the resolved Mendix +// version. No external deps: uses the global fetch + a small dotted-version comparator, so it +// runs on the runner's preinstalled node with no setup step. + +import fs from "node:fs"; + +const mendixVersion = process.argv[2]; +if (!mendixVersion) { + console.error("Usage: determine-nt-version.mjs "); + process.exit(1); +} + +const baseMatch = mendixVersion.match(/(\d+\.\d+\.\d+)/); +if (!baseMatch) { + console.error(`::error::Could not parse a X.Y.Z version from "${mendixVersion}"`); + process.exit(1); +} +const mendixVersionBase = baseMatch[1]; + +console.log(`Mendix version: ${mendixVersion}`); +console.log(`Mendix base version: ${mendixVersionBase}`); + +// Compare two dotted numeric versions (e.g. "11.11.0"). Returns 1 / 0 / -1. +// Non-numeric segments coerce to 0, mirroring the lenient parse the Python relied on. +function cmpVer(a, b) { + const pa = a.split(".").map(n => parseInt(n, 10)); + const pb = b.split(".").map(n => parseInt(n, 10)); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const x = pa[i] || 0; + const y = pb[i] || 0; + if (x > y) return 1; + if (x < y) return -1; + } + return 0; +} + +let versionData; +try { + versionData = JSON.parse(fs.readFileSync("mendix_version.json", "utf8")); +} catch { + console.error("::error::mendix_version.json not found"); + process.exit(1); +} + +function writeOutput(branch) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `nt_branch=${branch}\n`); +} + +// Pick the most specific ">=X.Y.Z" range the Mendix version satisfies (highest min wins). +let bestMatch = null; +let highestMin = "0.0.0"; +for (const rangeStr of Object.keys(versionData)) { + if (!rangeStr.startsWith(">=")) continue; + const minVer = rangeStr.slice(2); + if (cmpVer(mendixVersionBase, minVer) >= 0 && cmpVer(minVer, highestMin) > 0) { + bestMatch = rangeStr; + highestMin = minVer; + console.log(`Found matching range: ${rangeStr}`); + } +} + +if (!bestMatch) { + // Was a silent `nt_branch=master`. Fail loudly instead of building against an unpinned + // moving target when the Mendix version matches no configured range. + console.error(`::error::No Native Template version range matched Mendix ${mendixVersionBase}; refusing to fall back to master.`); + process.exit(1); +} + +console.log(`Best matching range: ${bestMatch}`); + +const maxPattern = versionData[bestMatch].max || ""; +let majorVersion = null; +if (maxPattern === "*") { + console.log("Looking for latest available release (no major version restriction)"); +} else { + majorVersion = maxPattern.split(".")[0]; + console.log(`Looking for latest release with major version: ${majorVersion}`); +} + +const response = await fetch("https://api.github.com/repos/mendix/native-template/releases", { + headers: { "User-Agent": "native-widgets-ci", Accept: "application/vnd.github+json" } +}); +if (!response.ok) { + // Was a silent `nt_branch=master`. Fail loudly so a transient API/rate-limit blip can't + // quietly pin the whole pipeline to master. + console.error(`::error::Failed to fetch native-template releases: HTTP ${response.status}`); + process.exit(1); +} + +const allReleases = await response.json(); +console.log(`Available releases: ${allReleases.map(r => r.tag_name).join(", ")}`); + +const matching = []; +for (const release of allReleases) { + const tag = release.tag_name; + const cleanTag = tag.startsWith("v") ? tag.slice(1) : tag; + if (!/^\d+\.\d+/.test(cleanTag)) continue; // skip non-version tags (the Python try/except) + if (majorVersion === null || cleanTag.startsWith(`${majorVersion}.`)) { + matching.push({ tag, clean: cleanTag }); + } +} + +if (matching.length > 0) { + matching.sort((a, b) => cmpVer(a.clean, b.clean)); + const latestTag = matching[matching.length - 1].tag; + console.log(`Selected Native Template release: ${latestTag}`); + writeOutput(latestTag); +} else { + // No published release for this range yet — fall back to the range's known-good minimum + // (a real pinned version, not master), matching the original behaviour. + const minVersion = versionData[bestMatch].min || ""; + console.log(`No matching release found, using minimum version: ${minVersion}`); + writeOutput(minVersion); +} diff --git a/.github/scripts/determine-nt-version.py b/.github/scripts/determine-nt-version.py deleted file mode 100644 index 16d9d1256..000000000 --- a/.github/scripts/determine-nt-version.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys -import json -import os -import requests -from packaging import version -import re - -mendix_version = sys.argv[1] -mendix_version_base = re.match(r'(\d+\.\d+\.\d+)', mendix_version).group(1) - -print(f"Mendix version: {mendix_version}") -print(f"Mendix base version: {mendix_version_base}") - -# Parse the JSON file -try: - with open('mendix_version.json', 'r') as file: - version_data = json.load(file) -except FileNotFoundError: - print("mendix_version.json not found") - sys.exit(1) - -# Find the best matching version range -best_match = None -highest_min_version = version.parse("0.0.0") -mendix_version_obj = version.parse(mendix_version_base) - -for range_str, data in version_data.items(): - if range_str.startswith(">="): - min_ver_str = range_str[2:] - min_ver = version.parse(min_ver_str) - - if mendix_version_obj >= min_ver and min_ver > highest_min_version: - best_match = range_str - highest_min_version = min_ver - print(f"Found matching range: {range_str}") - -if best_match: - print(f"Best matching range: {best_match}") - - # Get the major version to look for in releases - max_pattern = version_data[best_match].get("max", "") - if max_pattern == "*": - major_version = None - print("Looking for latest available release (no major version restriction)") - else: - major_version = max_pattern.split('.')[0] - print(f"Looking for latest release with major version: {major_version}") - - # Get available releases from native-template repository - response = requests.get('https://api.github.com/repos/mendix/native-template/releases') - if response.status_code == 200: - all_releases = response.json() - release_names = [release['tag_name'] for release in all_releases] - print(f"Available releases: {release_names}") - - # Find the latest release matching the major version - matching_releases = [] - - for release in all_releases: - tag = release['tag_name'] - clean_tag = tag[1:] if tag.startswith('v') else tag - try: - tag_version = version.parse(clean_tag) - if major_version is None or clean_tag.startswith(f"{major_version}."): - matching_releases.append((tag, tag_version)) - except: - pass - - if matching_releases: - matching_releases.sort(key=lambda x: x[1]) - latest_tag = matching_releases[-1][0] - print(f"Selected Native Template release: {latest_tag}") - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"nt_branch={latest_tag}\n") - else: - min_version = version_data[best_match].get("min", "") - print(f"No matching release found, using minimum version: {min_version}") - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"nt_branch={min_version}\n") - else: - print(f"Failed to get releases: {response.status_code}") - print("Using master as fallback") - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write("nt_branch=master\n") -else: - print("No matching version range found, using master") - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write("nt_branch=master\n") \ No newline at end of file diff --git a/.github/scripts/determine-widget-scope.sh b/.github/scripts/determine-widget-scope.sh index 4983cd507..f8c126200 100644 --- a/.github/scripts/determine-widget-scope.sh +++ b/.github/scripts/determine-widget-scope.sh @@ -18,64 +18,101 @@ all_widgets=$(echo "$widget_dirs" | jq -R -s -c 'split("\n") | map(select(length # Combined widgets and JS actions for default cases all_widgets_and_js=$(echo "$widget_dirs" | jq -R -s -c 'split("\n") | map(select(length > 0)) + ["mobile-resources-native", "nanoflow-actions-native"]') +# Convert a space-separated list of widget names into a JSON array (or [] when empty). +to_json_array() { + if [[ -z "$1" ]]; then + echo "[]" + else + echo "[\"$(echo "$1" | sed 's/ /","/g')\"]" + fi +} + if [ "$event_name" == "pull_request" ]; then + # Diff against the MERGE-BASE with the PR base branch so every commit in the PR is + # considered, not just the latest. `before_commit` is the PR base SHA + # (github.event.pull_request.base.sha); the merge-base is where the PR branched off. + # Requires full history — the scope job checks out with fetch-depth: 0. + diff_base="" if git cat-file -e "$before_commit" 2>/dev/null; then - changed_files=$(git diff --name-only "$before_commit" "$current_commit") + diff_base=$(git merge-base "$before_commit" "$current_commit" 2>/dev/null || echo "$before_commit") + fi + if [ -n "$diff_base" ]; then + echo "Diffing against PR merge-base: $diff_base" + changed_files=$(git diff --name-only "$diff_base" "$current_commit") else - echo "Previous commit not found, using HEAD~1 as fallback" + echo "PR base commit unavailable, using HEAD~1 as fallback" changed_files=$(git diff --name-only HEAD~1 "$current_commit") fi + # selected_workspaces = widgets to TEST (any change under the widget folder). + # build_workspaces = widgets to BUILD (only when a build-affecting file changed). selected_workspaces="" + build_workspaces="" js_actions_changed=false - + for file in $changed_files; do if [[ $file == packages/pluggableWidgets/* ]]; then widget=$(echo $file | cut -d'/' -f3) + subdir=$(echo $file | cut -d'/' -f4) if [[ ! $selected_workspaces =~ $widget ]]; then selected_workspaces="$selected_workspaces $widget" fi + # A change confined to the widget's e2e/ folder (Maestro flows + screenshots) changes + # only the TEST, not the built artifact — so it should NOT trigger a rebuild. The widget + # still goes into selected_workspaces above (it gets TESTED), but it's kept out of the + # BUILD scope; its .mpk comes from the test project's committed baseline, exactly like + # every other not-rebuilt widget in a partial run. + if [[ "$subdir" != "e2e" ]] && [[ ! $build_workspaces =~ $widget ]]; then + build_workspaces="$build_workspaces $widget" + fi elif [[ $file == packages/jsActions/mobile-resources-native/* ]] || [[ $file == packages/jsActions/nanoflow-actions-native/* ]]; then js_actions_changed=true fi done - # Trim leading and trailing spaces from selected_workspaces + # Trim leading and trailing spaces selected_workspaces=$(echo $selected_workspaces | xargs) + build_workspaces=$(echo $build_workspaces | xargs) # Build the final scope and widgets output # Note: widgets output is used for both BUILDING and the widget TEST MATRIX # When only JS actions change, widgets_to_test is empty (no widget tests needed) # but we still need to build all widgets for the test project + # full_build=true when every widget is (re)built — used by the resources job to safely + # restore/save a content-hashed dist cache. It is false for partial builds (a specific + # subset of widgets), whose dist set is incomplete and must not populate the shared cache. if [[ -n "$selected_workspaces" ]] && [[ "$js_actions_changed" == "true" ]]; then - # Both widgets and JS actions changed - # Convert space-separated widget names to JSON array format - widget_array=$(echo "$selected_workspaces" | sed 's/ /","/g') + # Both widgets and JS actions changed. Build only build-affected widgets (+ JS actions), + # but test every changed widget. build_workspaces may be empty (all changes were e2e-only), + # in which case no widgets are rebuilt and they test against the baseline project. echo "scope=--all --include '$selected_workspaces mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT - echo "widgets=[\"$widget_array\"]" >> $GITHUB_OUTPUT - echo "widgets_to_test=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "widgets=$(to_json_array "$build_workspaces")" >> $GITHUB_OUTPUT + echo "widgets_to_test=$(to_json_array "$selected_workspaces")" >> $GITHUB_OUTPUT echo "js_actions_changed=true" >> $GITHUB_OUTPUT + echo "full_build=false" >> $GITHUB_OUTPUT elif [[ -n "$selected_workspaces" ]] && [[ "$js_actions_changed" == "false" ]]; then - # Only widgets changed - widget_array=$(echo "$selected_workspaces" | sed 's/ /","/g') + # Only widgets changed. Build only build-affected widgets; test every changed widget. echo "scope=--all --include '$selected_workspaces'" >> $GITHUB_OUTPUT - echo "widgets=[\"$widget_array\"]" >> $GITHUB_OUTPUT - echo "widgets_to_test=[\"$widget_array\"]" >> $GITHUB_OUTPUT + echo "widgets=$(to_json_array "$build_workspaces")" >> $GITHUB_OUTPUT + echo "widgets_to_test=$(to_json_array "$selected_workspaces")" >> $GITHUB_OUTPUT echo "js_actions_changed=false" >> $GITHUB_OUTPUT + echo "full_build=false" >> $GITHUB_OUTPUT elif [[ -z "$selected_workspaces" ]] && [[ "$js_actions_changed" == "true" ]]; then - # Only JS actions changed - need to build ALL widgets because JS action tests + # Only JS actions changed - need to build ALL widgets because JS action tests # require the full test project with all widgets to function properly # But widget tests should NOT run (empty widgets_to_test) echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT echo "widgets_to_test=[]" >> $GITHUB_OUTPUT echo "js_actions_changed=true" >> $GITHUB_OUTPUT + echo "full_build=true" >> $GITHUB_OUTPUT else # No specific changes detected in widgets or JS actions, run everything echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT echo "widgets_to_test=${all_widgets}" >> $GITHUB_OUTPUT echo "js_actions_changed=true" >> $GITHUB_OUTPUT + echo "full_build=true" >> $GITHUB_OUTPUT fi else if [ -n "$input_workspace" ] && [ "$input_workspace" != "*-native" ] && [ "$input_workspace" != "js-actions" ]; then @@ -85,6 +122,7 @@ else echo "widgets=[\"$input_workspace\"]" >> $GITHUB_OUTPUT echo "widgets_to_test=[\"$input_workspace\"]" >> $GITHUB_OUTPUT echo "js_actions_changed=false" >> $GITHUB_OUTPUT + echo "full_build=false" >> $GITHUB_OUTPUT elif [ "$input_workspace" == "js-actions" ]; then # JS actions selected - need to build ALL widgets because JS action tests # require the full test project with all widgets to function properly @@ -93,12 +131,14 @@ else echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT echo "widgets_to_test=[]" >> $GITHUB_OUTPUT echo "js_actions_changed=true" >> $GITHUB_OUTPUT + echo "full_build=true" >> $GITHUB_OUTPUT else # All widgets (*-native) or default - run everything echo "scope=--all --include '*-native mobile-resources-native nanoflow-actions-native'" >> $GITHUB_OUTPUT echo "widgets=${all_widgets}" >> $GITHUB_OUTPUT echo "widgets_to_test=${all_widgets}" >> $GITHUB_OUTPUT echo "js_actions_changed=true" >> $GITHUB_OUTPUT + echo "full_build=true" >> $GITHUB_OUTPUT fi fi diff --git a/.github/scripts/setup-runtime.sh b/.github/scripts/setup-runtime.sh deleted file mode 100644 index 72b26aa76..000000000 --- a/.github/scripts/setup-runtime.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e - -MDA_FILE="$1" -MENDIX_VERSION="$2" -JAVA_PATH="$3" -WORKSPACE="$4" - -rm -rf project var tmp bin - -mkdir project -unzip -qq "$MDA_FILE" -d project -cp configs/e2e/m2ee-native.yml project/m2ee-native.yml -sed -i -- "s=\$ROOT_PATH=$WORKSPACE=g" project/m2ee-native.yml -sed -i -- "s=\$JAVA_HOME=$JAVA_PATH=g" project/m2ee-native.yml - -mkdir -p var/log var/opt/m2ee var/run bin tmp -# Clone m2ee-tools only if not cached -if [ ! -d tmp/m2ee ]; then - git clone https://github.com/KevinVlaanderen/m2ee-tools.git tmp/m2ee -fi -mv tmp/m2ee/src/* var/opt/m2ee -chmod a=rwx var/log/ var/run/ -echo "#!/bin/bash -x" > bin/m2ee -echo "python3 var/opt/m2ee/m2ee.py \$@" >>bin/m2ee -chmod +x bin/m2ee - -# Download only if not cached -mkdir -p "$WORKSPACE/project/runtimes" "$WORKSPACE/project/data/model-upload" "$WORKSPACE/project/data/database" "$WORKSPACE/project/data/files" "$WORKSPACE/project/data/tmp" -if [ ! -f tmp/runtime.tar.gz ]; then - wget -q "https://cdn.mendix.com/runtime/mendix-$MENDIX_VERSION.tar.gz" -O tmp/runtime.tar.gz -fi -tar xfz tmp/runtime.tar.gz --directory "$WORKSPACE/project/runtimes" \ No newline at end of file diff --git a/.github/scripts/start-runtime-with-verification.sh b/.github/scripts/start-runtime-with-verification.sh deleted file mode 100644 index cb6f181e3..000000000 --- a/.github/scripts/start-runtime-with-verification.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e - -# Arguments -MDA_FILE="$1" -MENDIX_VERSION="$2" -JAVA_PATH="$3" -WORKSPACE="$4" - -# Ensure setup script is executable -chmod +x .github/scripts/setup-runtime.sh - -# Run setup -.github/scripts/setup-runtime.sh "$MDA_FILE" "$MENDIX_VERSION" "$JAVA_PATH" "$WORKSPACE" - -# Stop any running Mendix runtime before starting a new one -if bin/m2ee -c "$WORKSPACE/project/m2ee-native.yml" status | grep -q "running"; then - echo "Stopping previous Mendix runtime..." - bin/m2ee -c "$WORKSPACE/project/m2ee-native.yml" stop - sleep 10 -fi - -# Start runtime -START_OUTPUT=$(bin/m2ee -c "$WORKSPACE/project/m2ee-native.yml" --verbose --yolo start 2>&1) -echo "Full output from start command:" -echo "$START_OUTPUT" -if [ $? -eq 0 ]; then - echo "Runtime started successfully (exit code)." - exit 0 -fi - -echo "Runtime did not start successfully." -exit 1 \ No newline at end of file diff --git a/.github/scripts/triage-test-results.sh b/.github/scripts/triage-test-results.sh new file mode 100755 index 000000000..6900ffeb4 --- /dev/null +++ b/.github/scripts/triage-test-results.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# Triage the widget/JS E2E shards of a finished run and print a Markdown report to stdout +# (the aggregate-test-results job redirects it into $GITHUB_STEP_SUMMARY). +# +# The plain pass/fail table doesn't tell a reviewer *why* a shard is red — and most reds in +# this pipeline are infra flakes (corrupt emulator image, runtime not up, missing artifact), +# not real test failures. This script classifies each non-green shard from its job metadata +# and, for the test step itself, from a grep over its log, so someone who didn't write this +# branch can tell "re-run it" from "an actual widget broke" at a glance. +# +# Usage: triage-test-results.sh [ []] +# repo defaults to $GITHUB_REPOSITORY +# run_id defaults to $GITHUB_RUN_ID +# Requires: gh (authenticated via $GH_TOKEN), jq. + +set -euo pipefail + +REPO="${1:-${GITHUB_REPOSITORY:?repo required}}" +RUN_ID="${2:-${GITHUB_RUN_ID:?run_id required}}" + +# Pull every shard job (with its steps) once. +jobs_json="$(gh api --paginate \ + "repos/${REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + --jq '[.jobs[] | select(.name | test("widget-tests|js-tests"))]' \ + | jq -s 'add // []')" + +# Cache the per-job log lookups so we hit the API at most once per failing shard. +fetch_log() { + local job_id="$1" cache + cache="$(mktemp)" + gh api "repos/${REPO}/actions/jobs/${job_id}/logs" >"$cache" 2>/dev/null || true + printf '%s' "$cache" +} + +# Sub-classify a failed "Run … tests" step from its log. Patterns are ordered most-specific +# first; an emulator that never boots can't also mismatch a screenshot, so they're exclusive. +classify_test_step() { + local log="$1" + if grep -qiE 'Error reading Zip content from a SeekableByteChannel|could not connect to TCP port 5554|Timeout waiting for emulator|emulator: ERROR' "$log"; then + echo '📵 Android emulator/adb never booted — corrupt system image (infra flake, re-run)' + elif grep -qE 'Smoke check FAILED' "$log"; then + echo '🚬 App did not launch — smoke check failed (build/bundle broken)' + elif grep -qiE 'threshold not met|Screenshot comparison failed|Comparison error: Assert screenshot' "$log"; then + echo '🖼 Screenshot mismatch — visual regression or stale baseline (REAL failure)' + elif grep -qiE 'driver.*(failed to start|did not start)|Unable to launch.*driver|Failed to connect to 127\.0\.0\.1' "$log"; then + echo '🤖 Maestro driver did not attach (flake)' + else + echo '❓ Test step failed — cause unclear, open the job log' + fi +} + +# Decide the triage cell for one job. Echoes "\t" where bucket is +# real | flake | none (none = green/skipped, not counted as a failure). +triage_job() { + local job="$1" + local conclusion failed_step job_id log cause + conclusion="$(jq -r '.conclusion // .status' <<<"$job")" + job_id="$(jq -r '.id' <<<"$job")" + + case "$conclusion" in + success) printf '—\tnone'; return ;; + skipped) printf '—\tnone'; return ;; + cancelled) + printf '⏱ Timed out — hit the per-shard timeout-minutes cap (slow/wedged shard)\tflake' + return ;; + esac + + # failure (or anything unexpected): lead with the step that actually failed. + failed_step="$(jq -r 'first(.steps[]? | select(.conclusion == "failure") | .name) // ""' <<<"$job")" + case "$failed_step" in + *Download*) printf '📦 Required artifact missing — upstream build did not publish it\tflake'; return ;; + *"Start Mendix runtime"*) printf '🔌 Mendix runtime never became ready\tflake'; return ;; + *"Run "*tests*) + log="$(fetch_log "$job_id")" + cause="$(classify_test_step "$log")" + rm -f "$log" + case "$cause" in + *REAL*|*broken*) printf '%s\treal' "$cause" ;; + *) printf '%s\tflake' "$cause" ;; + esac + return ;; + "") printf '❓ Failed, but no single step is marked failed — open the job log\tflake'; return ;; + *) printf '🧰 Setup/infra step failed: %s\tflake' "$failed_step"; return ;; + esac +} + +result_badge() { + case "$1" in + success) echo '✅ pass' ;; + failure) echo '❌ fail' ;; + cancelled) echo '🚫 cancelled' ;; + skipped) echo '⏭️ skipped' ;; + *) echo "$1" ;; + esac +} + +# --- Render ----------------------------------------------------------------------------- +# Sort so the rows that need a human come first: real failures, then infra/flake, then green. +# Each emitted line is "\t"; we sort on priority then strip it. +pass=0; real=0; flake=0; other=0 +rows="" +while IFS= read -r job; do + [ -z "$job" ] && continue + name="$(jq -r '.name' <<<"$job")" + conclusion="$(jq -r '.conclusion // .status' <<<"$job")" + IFS=$'\t' read -r cause bucket <<<"$(triage_job "$job")" + case "$conclusion" in + success|skipped) pass=$((pass + 1)); prio=3 ;; + *) case "$bucket" in + real) real=$((real + 1)); prio=0 ;; + flake) flake=$((flake + 1)); prio=1 ;; + *) other=$((other + 1)); prio=2 ;; + esac ;; + esac + rows+="${prio} | ${name} | $(result_badge "$conclusion") | ${cause} |"$'\n' +done < <(jq -c '.[]' <<<"$jobs_json") + +echo "## Native widget E2E results" +echo "" +echo "**${pass} green** · **${real} real failure(s)** 🖼🚬 · **${flake} likely infra/flake** (usually clears on re-run)" +echo "" +echo "| Shard | Result | Likely cause |" +echo "| --- | --- | --- |" +# Stable sort by priority (col 1), keeping discovery order within a bucket; drop the key column. +printf '%s' "$rows" | sort -s -t$'\t' -k1,1n | cut -f2- +echo "" +echo "🖼 screenshot mismatch · 🚬 app/build broken · 📵 emulator · 🔌 runtime · 📦 artifact · 🤖 driver · ⏱ timeout. " +echo "Only 🖼 and 🚬 point at the code under test; the rest are environment/flake and a re-run usually clears them." diff --git a/.github/workflows/NativePipeline.yml b/.github/workflows/NativePipeline.yml index b7fc6e929..8db9e1101 100644 --- a/.github/workflows/NativePipeline.yml +++ b/.github/workflows/NativePipeline.yml @@ -15,10 +15,20 @@ on: default: "" required: false type: string + test_project_ref: + description: "mendix/Native-Mobile-Resources ref (commit SHA) to build against (leave empty for the pinned default)" + default: "" + required: false + type: string LOCAL_TEST_ARTIFACT_RUN_ID: description: "Workflow run ID for local artifact download (leave empty for normal CI). Use the full run ID from the URL, not the run number displayed in UI." required: false default: "" + update_baselines: + description: "Capture new screenshot baselines instead of asserting. Tests still run (to produce screenshots) but visual mismatches won't fail the run; download the *-screenshots-* artifacts and commit them under maestro/images/expected/." + required: false + default: false + type: boolean workspace: description: "Select a widget to test (Default will run all)" required: true @@ -76,30 +86,49 @@ on: branches: - main -# Use default name in case no input -run-name: ${{ github.event.inputs.run_name || 'Run Native Pipeline' }} +# Surface capture mode in the run title (workflow_dispatch inputs aren't shown in the UI), +# otherwise fall back to the provided run_name / default. inputs.update_baselines is the +# typed boolean from workflow_dispatch (null on pull_request). +run-name: ${{ inputs.update_baselines && '🔄 Capture screenshot baselines' || (github.event.inputs.run_name || 'Run Native Pipeline') }} +# Cancel superseded runs for the same PR/branch; a new push should not race an in-flight pipeline. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true env: LOCAL_TEST_ARTIFACT_RUN_ID: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID || '' }} + # Opt out of Maestro's anonymous analytics (avoids a network call, keeps runs deterministic). + MAESTRO_CLI_NO_ANALYTICS: "1" + # When "true" (set via the update_baselines dispatch input), the maestro run steps tolerate + # assertScreenshot failures so a capture run can produce fresh baselines without going red. + UPDATE_BASELINES: ${{ github.event.inputs.update_baselines || 'false' }} + # Pin the test project (mendix/Native-Mobile-Resources) to a SHA for reproducibility instead + # of tracking a moving `main`. Single source of truth — bump deliberately. The repo publishes + # no tags, so a commit SHA is the only stable ref. Override per run via the dispatch input. + NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || 'f0e7c2278578022cc742f82a087e6c4fff2f7159' }} permissions: packages: write jobs: scope: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 outputs: scope: ${{ steps.scope.outputs.scope }} widgets: ${{ steps.scope.outputs.widgets }} widgets_to_test: ${{ steps.scope.outputs.widgets_to_test }} js_actions_changed: ${{ steps.scope.outputs.js_actions_changed }} + full_build: ${{ steps.scope.outputs.full_build }} steps: - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 2 # Fetch the latest two commits and its parent commit + # Full history so the scope script can diff against the PR base merge-base + # (covers every commit in the PR, not just the latest). + fetch-depth: 0 - name: "Determine scope" id: scope + # Pass the PR base SHA (empty on non-PR events → the script's dispatch path handles it). run: | chmod +x ./.github/scripts/determine-widget-scope.sh - ./.github/scripts/determine-widget-scope.sh "${{ github.event_name }}" "${{ github.event.inputs.workspace }}" "${{ github.event.before }}" "${{ github.sha }}" + ./.github/scripts/determine-widget-scope.sh "${{ github.event_name }}" "${{ github.event.inputs.workspace }}" "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}" - name: "Debug Scope Output" run: | echo "Scope is: ${{ steps.scope.outputs.scope }}" @@ -107,7 +136,7 @@ jobs: echo "Widgets to test: ${{ steps.scope.outputs.widgets_to_test }}" echo "JS actions changed: ${{ steps.scope.outputs.js_actions_changed }}" mendix-version: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: mendix_version: ${{ steps.set-mendix-version.outputs.MENDIX_VERSION }} steps: @@ -117,17 +146,17 @@ jobs: id: set-mendix-version run: | if [[ -n "${{ github.event.inputs.mendix_version }}" ]]; then - echo "MENDIX_VERSION=${{ github.event.inputs.mendix_version }}" >> $GITHUB_OUTPUT + echo "MENDIX_VERSION=${{ github.event.inputs.mendix_version }}" >> "$GITHUB_OUTPUT" else MENDIX_VERSION=$(jq -r '.latest' configs/e2e/mendix-versions.json) - echo "MENDIX_VERSION=$MENDIX_VERSION" >> $GITHUB_OUTPUT + echo "MENDIX_VERSION=$MENDIX_VERSION" >> "$GITHUB_OUTPUT" fi - name: "Debug Mendix Version" run: | echo "Mendix Version: ${{ steps.set-mendix-version.outputs.MENDIX_VERSION }}" determine-nt-version: needs: [mendix-version] - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 outputs: nt_branch: ${{ steps.set-output.outputs.nt_branch }} steps: @@ -138,11 +167,11 @@ jobs: run: | if [[ -n "${{ github.event.inputs.nt_branch }}" ]]; then echo "Using specified nt_branch: ${{ github.event.inputs.nt_branch }}" - echo "nt_branch=${{ github.event.inputs.nt_branch }}" >> $GITHUB_OUTPUT - echo "source=input" >> $GITHUB_OUTPUT + echo "nt_branch=${{ github.event.inputs.nt_branch }}" >> "$GITHUB_OUTPUT" + echo "source=input" >> "$GITHUB_OUTPUT" else echo "No nt_branch specified, will determine from mendix_version" - echo "source=auto" >> $GITHUB_OUTPUT + echo "source=auto" >> "$GITHUB_OUTPUT" fi - name: "Download mendix_version.json from native-template repo" if: steps.check-input.outputs.source == 'auto' @@ -152,23 +181,22 @@ jobs: - name: "Determine Native Template version based on Mendix version" if: steps.check-input.outputs.source == 'auto' id: determine-nt-branch - run: | - pip install requests packaging - python ./.github/scripts/determine-nt-version.py "${{ needs.mendix-version.outputs.mendix_version }}" + # Node (preinstalled on the runner) — no setup-python / pip; the script has no deps. + run: node ./.github/scripts/determine-nt-version.mjs "${{ needs.mendix-version.outputs.mendix_version }}" - name: "Set output nt_branch" id: set-output run: | if [[ "${{ steps.check-input.outputs.source }}" == "input" ]]; then - echo "nt_branch=${{ github.event.inputs.nt_branch }}" >> $GITHUB_OUTPUT + echo "nt_branch=${{ github.event.inputs.nt_branch }}" >> "$GITHUB_OUTPUT" else - echo "nt_branch=${{ steps.determine-nt-branch.outputs.nt_branch }}" >> $GITHUB_OUTPUT + echo "nt_branch=${{ steps.determine-nt-branch.outputs.nt_branch }}" >> "$GITHUB_OUTPUT" fi - name: "Debug final branch output" run: | echo "Final nt_branch value: ${{ steps.set-output.outputs.nt_branch }}" docker-images: needs: [mendix-version] - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Login to GitHub Container Registry" uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 @@ -178,8 +206,8 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: "Check if docker image already exists" run: | - docker manifest inspect ghcr.io/mendix/native-widgets/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} || EXIT_CODE=$? - echo "IMAGE_MISSING=$EXIT_CODE" >> $GITHUB_ENV + docker manifest inspect ghcr.io/${{ github.repository }}/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} || EXIT_CODE=$? + echo "IMAGE_MISSING=$EXIT_CODE" >> "$GITHUB_ENV" - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 if: ${{ env.IMAGE_MISSING != 0 }} @@ -192,10 +220,10 @@ jobs: build-args: | MENDIX_VERSION=${{ needs.mendix-version.outputs.mendix_version }} push: true - tags: ghcr.io/mendix/native-widgets/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} + tags: ghcr.io/${{ github.repository }}/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} resources: needs: [scope, mendix-version] - runs-on: ubuntu-22.04 + runs-on: ubuntu-26.04 permissions: packages: read contents: read @@ -204,39 +232,77 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - name: "Set up node" - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - name: "Setup Node and dependencies (with pnpm store cache)" + uses: ./.github/actions/setup-node-with-cache + - name: "Restore built resources (dist) cache" + id: resources-cache + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: - node-version-file: .nvmrc - package-manager-cache: false - - name: "Setup pnpm" - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 - - name: "Install dependencies" - run: pnpm install --frozen-lockfile - - name: "Force rebuild resources" + # Built widget .mpk / JS-action dist are reproducible from source + toolchain, so + # key on widget/JS-action src + their package.json + the lockfile. An infra-only + # change (no src touched) keeps the same key → exact hit → skip the rebuild below. + path: | + packages/pluggableWidgets/*/dist + packages/jsActions/*/dist + key: resources-dist-v1-${{ hashFiles('packages/pluggableWidgets/*/src/**', 'packages/jsActions/*/src/**', 'packages/pluggableWidgets/*/package.json', 'packages/jsActions/*/package.json', 'pnpm-lock.yaml') }} + - name: "Build resources" + # On a full build with an exact cache hit every widget's dist is already current, so + # skip the slow rollup/babel rebuild. Otherwise build the scoped widgets + JS actions + # in one pnpm invocation (multiple --filter targets run concurrently). run: | - # Build JS actions if needed (when js_actions_changed is true OR when workspace explicitly includes them) + if [ "${{ needs.scope.outputs.full_build }}" = "true" ] && [ "${{ steps.resources-cache.outputs.cache-hit }}" = "true" ]; then + echo "Full build with cached resources — skipping rebuild." + exit 0 + fi + + filters=() if [ "${{ needs.scope.outputs.js_actions_changed }}" = "true" ] || \ [ "${{ github.event.inputs.workspace }}" = "js-actions" ] || \ [ "${{ github.event.inputs.workspace }}" = "*-native" ] || \ [ "${{ github.event_name }}" = "schedule" ]; then - pnpm --filter=mobile-resources-native run build - pnpm --filter=nanoflow-actions-native run build + filters+=(--filter=mobile-resources-native --filter=nanoflow-actions-native) fi + while read -r w; do + [ -n "$w" ] && filters+=(--filter="$w") + done < <(echo '${{ needs.scope.outputs.widgets }}' | jq -r '.[]') - # Build widgets from scope - widgets=$(echo '${{ needs.scope.outputs.widgets }}' | jq -r '.[]') - for w in $widgets; do - pnpm --filter=$w run build - done + if [ ${#filters[@]} -gt 0 ]; then + echo "Building: ${filters[*]}" + pnpm "${filters[@]}" run build + else + echo "Nothing to build." + fi - name: "Unit test" run: pnpm -r --filter="${{ needs.scope.outputs.scope }}" run test + - name: "Save built resources (dist) cache" + # Only after a full build (every widget built), so a partial build never populates the + # shared cache with an incomplete dist set under a key a later full build could hit. + if: ${{ needs.scope.outputs.full_build == 'true' && steps.resources-cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + packages/pluggableWidgets/*/dist + packages/jsActions/*/dist + key: ${{ steps.resources-cache.outputs.cache-primary-key }} + - name: "Write resources manifest" + # Guarantees the resources artifact always contains at least one file, so the + # download in the `project` job never fails. A widgets-PR whose only changes are + # under e2e/ builds nothing (those widgets test against the baseline project), which + # would otherwise leave the artifact empty → not created → project download error. + run: | + { + echo "built widgets: ${{ needs.scope.outputs.widgets }}" + echo "tested widgets: ${{ needs.scope.outputs.widgets_to_test }}" + echo "js_actions_changed: ${{ needs.scope.outputs.js_actions_changed }}" + echo "full_build: ${{ needs.scope.outputs.full_build }}" + } > resources-manifest.txt - name: "Upload JS actions resources artifact" if: ${{ github.event.inputs.workspace == 'js-actions' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: name: resources path: | + resources-manifest.txt packages/jsActions/mobile-resources-native/dist/**/* packages/jsActions/nanoflow-actions-native/dist/**/* - name: "Upload widget and JS actions resources artifact" @@ -245,28 +311,34 @@ jobs: with: name: resources path: | + resources-manifest.txt packages/pluggableWidgets/**/dist/*/*.mpk packages/jsActions/mobile-resources-native/dist/**/* packages/jsActions/nanoflow-actions-native/dist/**/* project: needs: [resources, mendix-version] - runs-on: ubuntu-22.04 + runs-on: ubuntu-26.04 container: - image: ghcr.io/mendix/native-widgets/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} + image: ghcr.io/${{ github.repository }}/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - - name: "Make sure curl is installed" + - name: "Install curl and unzip" run: | - apt update && apt upgrade -y - apt install curl -y + apt update + apt install -y curl unzip - name: "Download test project" - run: curl -L -o project.zip https://github.com/mendix/Native-Mobile-Resources/archive/refs/heads/main.zip + # Pinned to a SHA (NATIVE_MOBILE_RESOURCES_REF) for reproducible builds. + run: curl -L -o project.zip "https://github.com/mendix/Native-Mobile-Resources/archive/${NATIVE_MOBILE_RESOURCES_REF}.zip" - name: "Extract test project" - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0 - with: - args: unzip -qq project.zip -d . + # GitHub names the archive's top dir after the ref (Native-Mobile-Resources-); + # rename it back to the stable Native-Mobile-Resources-main that the rest of the job + # references, so pinning the ref doesn't ripple through every downstream path. + run: | + unzip -qq project.zip -d . + rm -rf Native-Mobile-Resources-main + mv "Native-Mobile-Resources-${NATIVE_MOBILE_RESOURCES_REF}" Native-Mobile-Resources-main - name: "Download resources artifact" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -274,17 +346,23 @@ jobs: path: resources - name: "List resources" run: ls -R resources - - name: "Move widgets" + - name: "Overlay built widget mpks" if: ${{ github.event.inputs.workspace != 'js-actions' }} shell: bash run: | if compgen -G 'resources/pluggableWidgets/**/dist/*/*.mpk' > /dev/null; then for oldPath in resources/pluggableWidgets/**/dist/*/*.mpk; do - newPath=Native-Mobile-Resources-main/widgets/$(basename $oldPath) - mv -f $oldPath $newPath + newPath="Native-Mobile-Resources-main/widgets/$(basename "$oldPath")" + mv -f "$oldPath" "$newPath" done - mx update-widgets --loose-version-check Native-Mobile-Resources-main/NativeComponentsTestProject.mpr fi + - name: "Register widgets in the test project" + # Run unconditionally: update-widgets must sync the project's widget definitions with the + # mpks in widgets/ regardless of whether THIS run overlaid fresh ones. Must NOT be nested + # in the mpk-move guard — a `js-actions` dispatch (or any run that builds no mpks) would + # then skip it and leave stale Atlas widget defs for the portable-app build. + shell: bash + run: mx update-widgets --loose-version-check Native-Mobile-Resources-main/NativeComponentsTestProject.mpr - name: "Move mobile-resources" shell: bash run: | @@ -297,63 +375,49 @@ jobs: if compgen -G 'resources/jsActions/mobile-resources-native/*' > /dev/null; then mv -f resources/jsActions/nanoflow-actions-native/* Native-Mobile-Resources-main/javascriptsource/nanoflowcommons/actions/ fi - - name: "Force rebuild test project" + - name: "Build portable app package (self-contained runtime)" + # Run this BEFORE the native-packager deploy: portable-app-package re-prepares and + # CLEANS the project's deployment/ directory, which would wipe the native bundles. + # app.zip lives outside deployment/, so the later deploy's clean doesn't touch it. + # Output must live inside the project root: mxbuild's WhitelistAware path check + # rejects arbitrary output paths for the portable-app-package target. run: | - mxbuild -o automation.mda --loose-version-check Native-Mobile-Resources-main/NativeComponentsTestProject.mpr - - name: "Upload MDA" + mxbuild --target=portable-app-package \ + -o Native-Mobile-Resources-main/app.zip \ + --loose-version-check Native-Mobile-Resources-main/NativeComponentsTestProject.mpr + - name: "Build native bundles (deploy + native-packager)" + # Compiles the model and emits the Android/iOS JS bundles in native-template-ready + # layout at deployment/native/bundle/{android,iOS}. Supported replacement for the old + # create-native-bundle hack. Runs LAST so nothing cleans deployment/ afterward. + run: | + mxbuild --target=deploy --native-packager \ + --loose-version-check Native-Mobile-Resources-main/NativeComponentsTestProject.mpr + echo "=== native bundle output ===" + ls -R Native-Mobile-Resources-main/deployment/native/bundle 2>/dev/null \ + || find Native-Mobile-Resources-main -name 'index.*.bundle' -o -name 'bundle' -type d 2>/dev/null + - name: "Upload Android bundle" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: - name: mda - path: automation.mda - android-bundle: - needs: [project, mendix-version] - runs-on: ubuntu-24.04 - # Skip this job if we're using artifacts from a specific run - if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' }} - container: - image: ghcr.io/mendix/native-widgets/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: "Check out code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: "Download deployment package" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: mda - - name: "Create Android bundle" - uses: ./.github/actions/create-native-bundle - with: - platform: android - mda-file: automation.mda - ios-bundle: - needs: [project, mendix-version] - runs-on: ubuntu-24.04 - # Skip this job if we're using artifacts from a specific run - if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' }} - container: - image: ghcr.io/mendix/native-widgets/mxbuild:${{ needs.mendix-version.outputs.mendix_version }} - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: "Check out code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: "Download project MDA file" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + name: android-bundle + path: Native-Mobile-Resources-main/deployment/native/bundle/android + if-no-files-found: error + - name: "Upload iOS bundle" + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: - name: mda - - name: "Create iOS bundle" - uses: ./.github/actions/create-native-bundle + name: ios-bundle + path: Native-Mobile-Resources-main/deployment/native/bundle/iOS + if-no-files-found: error + - name: "Upload portable app package" + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: - platform: ios - mda-file: automation.mda + name: app-package + path: Native-Mobile-Resources-main/app.zip android-app: - needs: [android-bundle, determine-nt-version] - runs-on: ubuntu-24.04 - # Skip this job if we're using artifacts from a specific run, but allow it to run if android-bundle was skipped - if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && always() && (needs.android-bundle.result == 'success') }} + needs: [project, determine-nt-version] + runs-on: ubuntu-26.04 + # Skip this job if we're using artifacts from a specific run; the native bundles now come + # from the project job (deploy + native-packager), so gate on project success. + if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && always() && (needs.project.result == 'success') }} steps: - name: Debug branch value run: echo "Using branch ${{ needs.determine-nt-version.outputs.nt_branch }}" @@ -363,6 +427,19 @@ jobs: repository: mendix/native-template ref: ${{ needs.determine-nt-version.outputs.nt_branch }} path: native-template + - name: "Detect Android NDK version from native-template" + shell: bash + run: | + set -euo pipefail + # native-template declares e.g. ndkVersion = "27.3.13750724" in android/build.gradle + NDK=$(grep -oE 'ndkVersion[[:space:]]*=?[[:space:]]*"[0-9.]+"' native-template/android/build.gradle \ + | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) + if [ -z "$NDK" ]; then + echo "::warning::Could not detect ndkVersion from native-template; gradle will resolve/download it." + else + echo "Detected NDK version: $NDK" + fi + echo "ANDROID_NDK_VERSION=$NDK" >> "$GITHUB_ENV" - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -387,8 +464,10 @@ jobs: ${{ runner.os }}-android-build- - name: "Copy files to the right location" run: | - mv bundles/android/index.android.bundle native-template/android/app/src/main/assets/index.android.bundle - cp -r bundles/android/assets/* native-template/android/app/src/main/res/ + # native-packager output is pre-split: bundle/android/{assets/index.android.bundle, res/...} + # maps directly onto app/src/main/{assets,res} (per the official manual-build doc). + mkdir -p native-template/android/app/src/main + cp -r bundles/android/* native-template/android/app/src/main/ mv native-widgets/configs/e2e/config.json native-template mv native-widgets/configs/e2e/google-services.json native-template/android/app node native-widgets/scripts/test/add-native-dependencies.js @@ -402,14 +481,45 @@ jobs: distribution: temurin cache: gradle - - name: "Build Android app" - working-directory: native-template/android + # Install the NDK explicitly and cache it (keyed on the version detected from + # native-template), instead of letting the gradle build download ~1GB every run. + # (${{ }} expressions are expanded by Actions, so they are safe in cache `path` + # — unlike shell $VARs.) If detection failed, these no-op and gradle resolves it. + - name: "Cache Android NDK" + id: cache-ndk + if: env.ANDROID_NDK_VERSION != '' + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: /usr/local/lib/android/sdk/ndk/${{ env.ANDROID_NDK_VERSION }} + key: ${{ runner.os }}-android-ndk-${{ env.ANDROID_NDK_VERSION }} + - name: "Install Android NDK" + if: env.ANDROID_NDK_VERSION != '' && steps.cache-ndk.outputs.cache-hit != 'true' + shell: bash run: | - ./gradlew assembleAppstoreDebug assembleAppstoreDebugAndroidTest - if [ $? -ne 0 ]; then - echo "Build failed!" - exit 1 + set -euo pipefail + SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}" + SDKMANAGER="$SDK/cmdline-tools/latest/bin/sdkmanager" + if [ ! -x "$SDKMANAGER" ]; then + SDKMANAGER="$(command -v sdkmanager || true)" fi + if [ -z "${SDKMANAGER:-}" ] || [ ! -x "$SDKMANAGER" ]; then + SDKMANAGER="$(find "$SDK" -type f -name sdkmanager 2>/dev/null | head -1)" + fi + echo "Using sdkmanager: $SDKMANAGER" + # Accept licenses first (yes can SIGPIPE under pipefail, so tolerate it), then install. + yes | "$SDKMANAGER" --licenses > /dev/null 2>&1 || true + "$SDKMANAGER" "ndk;${ANDROID_NDK_VERSION}" + + - name: "Build Android app" + working-directory: native-template/android + # The CI emulator is x86_64, so only compile that ABI's native code instead of all + # four (arm64-v8a, armeabi-v7a, x86, x86_64) — cuts the C/C++ build by ~75%. + # + # --build-cache turns on Gradle's local build cache (task-output content cache). It lives + # under ~/.gradle/caches/build-cache-1, already persisted by setup-java's `cache: gradle`, + # so unchanged tasks (incl. RN libs' native compiles, which don't change run-to-run) are + # reused instead of recompiled. Safe: a cache miss just rebuilds. + run: ./gradlew assembleAppstoreDebug assembleAppstoreDebugAndroidTest -PreactNativeArchitectures=x86_64 --build-cache - name: "List APK files" run: | echo "Listing APK files in the output directory:" @@ -420,10 +530,11 @@ jobs: name: android-app path: native-template/android/app/build/outputs/apk/**/*.apk ios-app: - needs: [ios-bundle, determine-nt-version] - runs-on: macos-15 - # Skip this job if we're using artifacts from a specific run, but allow it to run if ios-bundle was skipped - if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && always() && (needs.ios-bundle.result == 'success') }} + needs: [project, determine-nt-version] + runs-on: macos-26 + # Skip this job if we're using artifacts from a specific run; the native bundles now come + # from the project job (deploy + native-packager), so gate on project success. + if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && always() && (needs.project.result == 'success') }} steps: - name: "Check out Native Template for Native Components Test Project" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -447,6 +558,8 @@ jobs: cache: npm cache-dependency-path: native-template/package-lock.json - name: "Cache iOS Build" + # Persists the DerivedData root (`build/`) so unchanged products can be reused across + # runs; restore-keys falls back to the latest entry on a partial-input change. uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: native-template/ios/build @@ -455,8 +568,10 @@ jobs: ${{ runner.os }}-ios-build- - name: "Copy files to the right location" run: | - mv bundles/ios/index.ios.bundle native-template/ios/Bundle/index.ios.bundle - mv bundles/ios/assets/assets native-template/ios/Bundle/ + # native-packager emits bundle/iOS/{index.ios.bundle, assets/...}; copy the lot into + # ios/Bundle (per the official manual-build doc). + mkdir -p native-template/ios/Bundle + cp -r bundles/ios/* native-template/ios/Bundle/ mv native-widgets/configs/e2e/config.json native-template node native-widgets/scripts/test/add-native-dependencies.js - name: "Install Node dependencies" @@ -466,7 +581,11 @@ jobs: - name: "Setup Pods cache" uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: - path: native-template/Pods + # Also cache CocoaPods' own download cache so `pod install` is fast even when + # the Pods/ dir misses. (~ is expanded by actions/cache.) + path: | + native-template/Pods + ~/Library/Caches/CocoaPods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} restore-keys: | ${{ runner.os }}-pods- @@ -475,20 +594,70 @@ jobs: run: pod install - name: "Build iOS app" working-directory: native-template/ios - run: xcodebuild -workspace NativeTemplate.xcworkspace -scheme nativeTemplate -configuration Debug -sdk iphonesimulator -derivedDataPath build ONLY_ACTIVE_ARCH=YES VALID_ARCHS="i386 x86_64" + # Build for the simulator's native arch (arm64 on Apple Silicon runners). + # ONLY_ACTIVE_ARCH=YES already limits to the active arch; the old + # VALID_ARCHS="i386 x86_64" forced dead/foreign archs and broke on macos-26. + run: xcodebuild -workspace NativeTemplate.xcworkspace -scheme nativeTemplate -configuration Debug -sdk iphonesimulator -derivedDataPath build ONLY_ACTIVE_ARCH=YES - name: "Archive iOS app" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: name: ios-app path: native-template/ios/build/Build/Products/**/*.app + android-avd-cache: + runs-on: ubuntu-26.04 + # Warm a booted-emulator snapshot once and cache it, so the per-widget Android shards + # restore it instead of cold-booting (~1-2 min saved per shard). Pure speed optimization: + # shards fall back to a cold boot on cache miss, so a failure here doesn't block tests. + steps: + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: "AVD cache" + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + # Keep in sync with the emulator config and the restore step in the test jobs. + key: avd-android-35-x86_64-google_apis-pixel + - name: "Create AVD and generate snapshot for caching" + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 + with: + api-level: 35 + target: google_apis + arch: x86_64 + profile: pixel + force-avd-creation: false + # 2 vCPUs for the emulator. GitHub-hosted Linux runners have only 4 vCPUs total, so + # leave 2 for the host (adb, Maestro, the JS bundler proxy). This is the action's + # default and the sweet spot on free runners; it's written to the AVD's config.ini + # (hw.cpu.ncore) and is what the emulator actually honors — a `-cores N` in + # emulator-options below would be silently overridden, so we set it here instead. + cores: 2 + emulator-options: "-no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096" + disable-animations: false + script: echo "Generated AVD snapshot for caching." + android-widget-tests: - needs: [scope, mendix-version, project, android-app] + needs: [scope, mendix-version, project, android-app, android-avd-cache] # Run if widgets need testing (widgets_to_test is not empty) and project succeeds if: ${{ needs.scope.outputs.widgets_to_test != '[]' && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 + runs-on: ubuntu-26.04 + # 30 min hard cap per widget shard. A healthy widget's flows run in ~30s each; the cap is a + # backstop for genuinely wedged shards. Bumped 20→30 after a run where shards were cancelled + # at 20 min — that was traced to a video-recording bug (the on-device screenrecord wasn't + # being stopped, adding ~2.5 min/flow); with that fixed 30 is comfortable headroom. If shards + # still hit 30, the cause is real app instability, not the cap — needs a different fix. + timeout-minutes: 30 strategy: + # 5 parallel Linux shards. Bounded by GitHub Free's 20 total concurrent-job cap (not the + # macOS-5 cap that limits the iOS matrix), so there's headroom. iOS stays at 4 to keep a + # macOS slot of headroom under that 5-job ceiling. max-parallel: 5 matrix: widget: ${{ fromJson(needs.scope.outputs.widgets_to_test) }} @@ -498,10 +667,10 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - name: "Download project MDA file" + - name: "Download portable app package" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: mda + name: app-package # Used only when specific run id is provided when triggering the jbo - name: "Download Android app from specific run" if: ${{ env.LOCAL_TEST_ARTIFACT_RUN_ID != '' }} @@ -521,22 +690,25 @@ jobs: - name: "Setup needed tools" id: setup-needed-tools uses: ./.github/actions/setup-tools - with: - mendix_version: ${{ needs.mendix-version.outputs.mendix_version }} - - name: "Start runtime with retry" - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - with: - timeout_minutes: 20 - max_attempts: 3 - command: | - chmod +x .github/scripts/start-runtime-with-verification.sh - .github/scripts/start-runtime-with-verification.sh automation.mda ${{ needs.mendix-version.outputs.mendix_version }} "${{ steps.setup-needed-tools.outputs.java-path }}" "${{ github.workspace }}" + - name: "Start Mendix runtime" + uses: ./.github/actions/start-mendix-runtime + with: + java-home: ${{ steps.setup-needed-tools.outputs.java-path }} - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: "Restore AVD snapshot" + # Restore-only: the android-avd-cache job is the sole writer, so shards don't + # each try to re-save the same key. Must match that job's key. + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-android-35-x86_64-google_apis-pixel - name: "Run android tests" uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 with: @@ -544,15 +716,24 @@ jobs: target: google_apis arch: x86_64 profile: pixel - emulator-options: "-no-window -gpu swiftshader_indirect -no-boot-anim -no-snapshot -memory 4096 -cores 4" + # Boot from the cached snapshot (don't overwrite it); falls back to a cold boot on cache miss. + force-avd-creation: false + # 2 vCPUs: free GitHub runners have 4 total; leave 2 for the host. Set via the action + # input (written to config.ini hw.cpu.ncore) — a `-cores N` emulator-option is overridden. + cores: 2 + emulator-options: "-no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 4096" disable-animations: true script: | chmod +x maestro/helpers/prepare_android.sh chmod +x maestro/run_maestro_widget_tests.sh bash maestro/helpers/prepare_android.sh - bash maestro/run_maestro_widget_tests.sh android "${{ matrix.widget }}" + bash maestro/run_maestro_widget_tests.sh android "${{ matrix.widget }}" || [ "$UPDATE_BASELINES" = "true" ] - name: Archive test results + # always(): the test step exits non-zero on a failing shard, which would otherwise + # SKIP this step — i.e. we'd capture screenshots/logs/videos for green shards but NOT + # for the failing ones we actually need to debug. Run regardless of test outcome. + if: ${{ always() }} uses: ./.github/actions/archive-test-results with: platform: android @@ -562,42 +743,20 @@ jobs: needs: [scope, mendix-version, project, ios-app] # Run if widgets need testing (widgets_to_test is not empty) and project succeeds if: ${{ needs.scope.outputs.widgets_to_test != '[]' && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} - runs-on: macos-15 - timeout-minutes: 60 + runs-on: macos-26 + # 30 min per widget shard, matching android-widget-tests. iOS needs the budget more than + # Android: sim cold-boot + Maestro driver startup + runtime-ready wait eat several minutes + # before any flow runs, so on a free macOS runner slow-but-healthy shards were getting + # truncated — at 20 min several shards (accordion, background-image, bottom-sheet, + # color-picker, safe-area-view) were cancelled mid-flow. If a shard still hits 30 the cause + # is real app instability, not the cap. + timeout-minutes: 30 strategy: - max-parallel: 5 + max-parallel: 4 matrix: widget: ${{ fromJson(needs.scope.outputs.widgets_to_test) }} fail-fast: false steps: - - name: "Force cleanup workspace" - run: | - # Kill all Git and Xcode processes aggressively - sudo pkill -9 -f git || true - sudo pkill -9 -f xcodebuild || true - sudo pkill -9 -f node || true - - # Wait for processes to be killed - sleep 5 - - # Clean the workspace contents while preserving directory structure - cd /Users/runner/work/native-widgets - if [ -d "native-widgets" ]; then - cd native-widgets - - # Force remove all git state and locks with extreme prejudice - sudo rm -rf .git || true - find . -name "*.lock" -type f -exec sudo rm -f {} \; 2>/dev/null || true - find . -name ".git*" -exec sudo rm -rf {} \; 2>/dev/null || true - - # Remove all other contents - sudo find . -mindepth 1 -not -path "./.git*" -delete 2>/dev/null || true - fi - - # Final wait and process cleanup - sudo pkill -9 -f git || true - sleep 3 - continue-on-error: true - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -605,10 +764,10 @@ jobs: fetch-tags: false clean: true ref: ${{ github.sha }} - - name: "Download project MDA file" + - name: "Download portable app package" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: mda + name: app-package # Used only for local testing, will be empty on GitHub - name: "Download iOS app from specific run" if: ${{ env.LOCAL_TEST_ARTIFACT_RUN_ID != '' }} @@ -628,16 +787,10 @@ jobs: - name: "Setup needed tools" id: setup-needed-tools uses: ./.github/actions/setup-tools + - name: "Start Mendix runtime" + uses: ./.github/actions/start-mendix-runtime with: - mendix_version: ${{ needs.mendix-version.outputs.mendix_version }} - - name: "Start runtime with retry" - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - command: | - chmod +x .github/scripts/start-runtime-with-verification.sh - .github/scripts/start-runtime-with-verification.sh automation.mda ${{ needs.mendix-version.outputs.mendix_version }} "${{ steps.setup-needed-tools.outputs.java-path }}" "${{ github.workspace }}" + java-home: ${{ steps.setup-needed-tools.outputs.java-path }} - name: "Setup Xcode CLI Tools" uses: ./.github/actions/setup-xcode-cli-tools @@ -646,29 +799,31 @@ jobs: chmod +x maestro/helpers/prepare_ios.sh chmod +x maestro/run_maestro_widget_tests.sh bash maestro/helpers/prepare_ios.sh - bash maestro/run_maestro_widget_tests.sh ios "${{ matrix.widget }}" + bash maestro/run_maestro_widget_tests.sh ios "${{ matrix.widget }}" || [ "$UPDATE_BASELINES" = "true" ] - name: Archive test results + # always(): capture failure artifacts (the failing shard's test step exits non-zero). + if: ${{ always() }} uses: ./.github/actions/archive-test-results with: platform: ios test-type: ${{ matrix.widget }} android-js-tests: - needs: [scope, mendix-version, project, android-app] + needs: [scope, mendix-version, project, android-app, android-avd-cache] # Run if JS actions changed and project succeeds and either android-app succeeds OR we're using custom artifacts (android-app was skipped) if: ${{ needs.scope.outputs.js_actions_changed == 'true' && always() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 90 steps: - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - name: "Download project MDA file" + - name: "Download portable app package" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: mda + name: app-package # Used only for local testing, will be empty on GitHub - name: "Download Android app from specific run" if: ${{ env.LOCAL_TEST_ARTIFACT_RUN_ID != '' }} @@ -688,21 +843,24 @@ jobs: - name: "Setup needed tools" id: setup-needed-tools uses: ./.github/actions/setup-tools + - name: "Start Mendix runtime" + uses: ./.github/actions/start-mendix-runtime with: - mendix_version: ${{ needs.mendix-version.outputs.mendix_version }} - - name: "Start runtime with retry" - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - command: | - chmod +x .github/scripts/start-runtime-with-verification.sh - .github/scripts/start-runtime-with-verification.sh automation.mda ${{ needs.mendix-version.outputs.mendix_version }} "${{ steps.setup-needed-tools.outputs.java-path }}" "${{ github.workspace }}" + java-home: ${{ steps.setup-needed-tools.outputs.java-path }} - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: "Restore AVD snapshot" + # Restore-only: the android-avd-cache job is the sole writer, so shards don't + # each try to re-save the same key. Must match that job's key. + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-android-35-x86_64-google_apis-pixel - name: "Run android tests" uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 with: @@ -710,15 +868,22 @@ jobs: target: google_apis arch: x86_64 profile: pixel - emulator-options: "-no-window -gpu swiftshader_indirect -no-boot-anim -no-snapshot -memory 4096 -cores 4" + # Boot from the cached snapshot (don't overwrite it); falls back to a cold boot on cache miss. + force-avd-creation: false + # 2 vCPUs: free GitHub runners have 4 total; leave 2 for the host. Set via the action + # input (written to config.ini hw.cpu.ncore) — a `-cores N` emulator-option is overridden. + cores: 2 + emulator-options: "-no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 4096" disable-animations: true script: | chmod +x maestro/helpers/prepare_android.sh chmod +x maestro/run_maestro_jsactions_tests.sh bash maestro/helpers/prepare_android.sh - bash maestro/run_maestro_jsactions_tests.sh android + bash maestro/run_maestro_jsactions_tests.sh android || [ "$UPDATE_BASELINES" = "true" ] - name: Archive test results + # always(): capture failure artifacts (the failing shard's test step exits non-zero). + if: ${{ always() }} uses: ./.github/actions/archive-test-results with: platform: android @@ -728,46 +893,19 @@ jobs: needs: [scope, mendix-version, project, ios-app] # Run if JS actions changed and project succeeds and either ios-app succeeds OR we're using custom artifacts (ios-app was skipped) if: ${{ needs.scope.outputs.js_actions_changed == 'true' && always() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} - runs-on: macos-15 + runs-on: macos-26 timeout-minutes: 90 steps: - - name: "Clean up any Git locks" - run: | - # Kill all git and related processes aggressively - sudo pkill -9 -f git || true - sudo pkill -9 -f node || true - - # Wait for processes to be killed - sleep 5 - - # Clean the workspace contents while preserving directory structure - cd /Users/runner/work/native-widgets - if [ -d "native-widgets" ]; then - cd native-widgets - - # Force remove git state and locks - sudo rm -rf .git || true - find . -name "*.lock" -type f -exec sudo rm -f {} \; 2>/dev/null || true - find . -name ".git*" -exec sudo rm -rf {} \; 2>/dev/null || true - - # Remove all other contents - sudo find . -mindepth 1 -not -path "./.git*" -delete 2>/dev/null || true - fi - - # Final cleanup - sudo pkill -9 -f git || true - sleep $((RANDOM % 5 + 3)) - continue-on-error: true - name: "Check out code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 fetch-tags: false clean: true - - name: "Download project MDA file" + - name: "Download portable app package" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: mda + name: app-package # Used only for local testing, will be empty on GitHub - name: "Download iOS app from specific run" if: ${{ env.LOCAL_TEST_ARTIFACT_RUN_ID != '' }} @@ -787,16 +925,10 @@ jobs: - name: "Setup needed tools" id: setup-needed-tools uses: ./.github/actions/setup-tools + - name: "Start Mendix runtime" + uses: ./.github/actions/start-mendix-runtime with: - mendix_version: ${{ needs.mendix-version.outputs.mendix_version }} - - name: "Start runtime with retry" - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - command: | - chmod +x .github/scripts/start-runtime-with-verification.sh - .github/scripts/start-runtime-with-verification.sh automation.mda ${{ needs.mendix-version.outputs.mendix_version }} "${{ steps.setup-needed-tools.outputs.java-path }}" "${{ github.workspace }}" + java-home: ${{ steps.setup-needed-tools.outputs.java-path }} - name: "Setup Xcode CLI Tools" uses: ./.github/actions/setup-xcode-cli-tools @@ -805,79 +937,91 @@ jobs: chmod +x maestro/helpers/prepare_ios.sh chmod +x maestro/run_maestro_jsactions_tests.sh bash maestro/helpers/prepare_ios.sh - bash maestro/run_maestro_jsactions_tests.sh ios + bash maestro/run_maestro_jsactions_tests.sh ios || [ "$UPDATE_BASELINES" = "true" ] - name: Archive test results + # always(): capture failure artifacts (the failing shard's test step exits non-zero). + if: ${{ always() }} uses: ./.github/actions/archive-test-results with: platform: ios test-type: js-actions - compare-screenshots: - needs: [scope, android-widget-tests, ios-widget-tests, android-js-tests, ios-js-tests] - runs-on: ubuntu-latest - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Each widget/JS shard uploads its own per-shard screenshots/logs/artifacts (one set of three + # per matrix entry → dozens of tiny artifacts). This job merges them into a handful of combined + # zips so a reviewer can grab everything in one download. `delete-merged` removes the per-shard + # sources after a successful merge, leaving just the combined artifacts. + aggregate-test-results: + needs: [android-widget-tests, ios-widget-tests, android-js-tests, ios-js-tests] + # Run even when shards fail/cancel — the failing shards are exactly the ones whose + # screenshots/logs we want to inspect. + if: ${{ always() }} + runs-on: ubuntu-26.04 + # The workflow-level block grants only packages:write, so every other scope defaults to + # none. This job needs contents:read (to check out the triage script) and actions:read + # (the script reads each shard's conclusion and logs via the Actions API). + permissions: + contents: read + actions: read steps: - name: "Check out code" + # Needed only for the triage script below; the merge steps don't touch the repo. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: "Download Android screenshots" - run: | - widgets=$(echo '${{ needs.scope.outputs.widgets_to_test }}' | jq -r '.[]') - mkdir -p images/actual/android/ - for widget in $widgets; do - echo "Downloading android-screenshots-${widget}" - gh run download ${{ github.run_id }} --name android-screenshots-${widget} --dir images/actual/android/ || true - ls -l images/actual/android/ - done - # Download JS actions screenshots - echo "Downloading android-screenshots-js-actions" - gh run download ${{ github.run_id }} --name android-screenshots-js-actions --dir images/actual/android/ || true - ls -l images/actual/android/ - - - name: "Download iOS screenshots" + with: + fetch-depth: 1 + - name: "Write test result summary" + # Triage table for every widget/JS shard, rendered on the run's Summary tab. Beyond + # pass/fail it classifies each red shard (timeout / emulator / runtime / artifact / + # screenshot mismatch / …) so a reviewer can tell a real failure from an infra flake + # without opening logs. Each shard's result lives in its own matrix job; the script + # reads them back from the API (this job `needs:` them all, so conclusions are final). + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - widgets=$(echo '${{ needs.scope.outputs.widgets_to_test }}' | jq -r '.[]') - mkdir -p images/actual/ios/ - for widget in $widgets; do - echo "Downloading ios-screenshots-${widget}" - gh run download ${{ github.run_id }} --name ios-screenshots-${widget} --dir images/actual/ios/ || true - ls -l images/actual/ios/ - done - # Download JS actions screenshots - echo "Downloading ios-screenshots-js-actions" - gh run download ${{ github.run_id }} --name ios-screenshots-js-actions --dir images/actual/ios/ || true - ls -l images/actual/ios/ - - - name: "Setup Node and dependencies" - uses: ./.github/actions/setup-node-with-cache - - - name: "Compare Android screenshots" - continue-on-error: true - run: node ${{ github.workspace }}/maestro/helpers/compare_screenshots.js android - - - name: "Compare iOS screenshots" + bash .github/scripts/triage-test-results.sh \ + "${{ github.repository }}" "${{ github.run_id }}" >> "$GITHUB_STEP_SUMMARY" + - name: "Merge screenshots" + uses: actions/upload-artifact/merge@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + # continue-on-error: a run that produced no screenshots (e.g. only JS-action tests) has + # nothing to merge and the action would otherwise hard-fail. Same for the two below. continue-on-error: true - run: node ${{ github.workspace }}/maestro/helpers/compare_screenshots.js ios - - - name: "Archive diff results" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 with: - name: screenshot-diffs - path: images/diffs - if-no-files-found: ignore - - - name: "Archive comparison results" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 + name: all-screenshots + pattern: "*-screenshots-*" + # Keep each shard's PNGs in its own subdir so identically-named actual/ files don't collide. + separate-directories: true + delete-merged: true + - name: "Merge screenshot diffs" + uses: actions/upload-artifact/merge@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + # Only mismatching flows produce a diff; a run with no screenshot failures has none. + continue-on-error: true with: - name: comparison-results - path: compare_output.txt - if-no-files-found: ignore - - - name: "Fail if comparisons failed" - run: | - if [ "$ANDROID_COMPARISON_FAILED" == "true" ] || [ "$IOS_COMPARISON_FAILED" == "true" ]; then - echo "One or more comparisons failed." - exit 1 - fi \ No newline at end of file + name: all-screenshot-diffs + pattern: "*-diffs-*" + separate-directories: true + delete-merged: true + - name: "Merge runtime logs" + uses: actions/upload-artifact/merge@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + continue-on-error: true + with: + name: all-runtime-logs + pattern: "*-runtime-logs-*" + separate-directories: true + delete-merged: true + - name: "Merge debug artifacts" + uses: actions/upload-artifact/merge@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + continue-on-error: true + with: + name: all-debug-artifacts + pattern: "*-artifacts-*" + separate-directories: true + delete-merged: true + - name: "Merge failure videos" + uses: actions/upload-artifact/merge@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + # Only failing flows leave a video behind; a fully-green run has none to merge. + continue-on-error: true + with: + name: all-failure-videos + pattern: "*-videos-*" + separate-directories: true + delete-merged: true diff --git a/configs/e2e/m2ee-native.yml b/configs/e2e/m2ee-native.yml deleted file mode 100644 index 258197a86..000000000 --- a/configs/e2e/m2ee-native.yml +++ /dev/null @@ -1,24 +0,0 @@ -m2ee: - app_name: Test App - app_base: $ROOT_PATH/project - admin_port: 8090 - admin_pass: Password1! - runtime_port: 8080 - runtime_listen_addresses: "*" - javaopts: - ["-Dfile.encoding=UTF-8", "-Xmx512M", "-Xms512M", "-Djava.io.tmpdir=$ROOT_PATH/project/data/tmp"] - javabin: $JAVA_HOME/bin/java - pidfile: $ROOT_PATH/var/run/m2ee.pid -mxruntime: - DTAPMode: T - ApplicationRootUrl: http://localhost:8080/ - DatabaseType: HSQLDB - DatabaseName: default - DatabasePassword: "" - BuiltInDatabasePath: $ROOT_PATH/project/data/database -logging: - - - type: file - name: FileSubscriber - autosubscribe: TRACE - filename: $ROOT_PATH/log/runtime.log \ No newline at end of file diff --git a/maestro/Precondition.yaml b/maestro/Precondition.yaml index cb09b2007..02d45b245 100644 --- a/maestro/Precondition.yaml +++ b/maestro/Precondition.yaml @@ -1,7 +1,7 @@ appId: "${APP_ID}" --- - retry: - maxRetries: 3 + maxRetries: 2 commands: - launchApp: clearState: true @@ -10,4 +10,8 @@ appId: "${APP_ID}" notifications: allow - extendedWaitUntil: visible: "Widgets menu" - timeout: 240000 # A long timeout since app startup is quite unstable due to resources when executed on Github runners + # 60s per attempt × 2 retries. Was 240s × 3 (= up to 12 min PER flow), which is what + # turned a broken/sluggish shard into a 60-min timeout. The up-front smoke check + # (Smoke.yaml) already absorbs a fundamentally-broken build; this keeps a small + # retry budget for genuine GitHub-runner sluggishness without the amplification. + timeout: 60000 diff --git a/maestro/Smoke.yaml b/maestro/Smoke.yaml new file mode 100644 index 000000000..f4f903288 --- /dev/null +++ b/maestro/Smoke.yaml @@ -0,0 +1,14 @@ +appId: "${APP_ID}" +--- +# Fast-fail smoke check (S6). Run ONCE per shard before any widget flows: if the app can't +# launch and render the "Widgets menu", the build/bundle is fundamentally broken and every +# flow would fail the same way. Bail in ~1.5 min instead of grinding 8-9 flows × retries up +# to the job timeout. Short timeout + NO retry block on purpose — this is the cheap probe. +- launchApp: + clearState: true + permissions: + camera: allow + notifications: allow +- extendedWaitUntil: + visible: "Widgets menu" + timeout: 60000 diff --git a/maestro/helpers/compare_screenshots.js b/maestro/helpers/compare_screenshots.js deleted file mode 100644 index 79fec777c..000000000 --- a/maestro/helpers/compare_screenshots.js +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable prettier/prettier */ -const fs = require("fs"); -const path = require("path"); -const { PNG } = require("pngjs"); -const pixelmatch = require("pixelmatch"); -const core = require("@actions/core"); - -const platform = process.argv[2]; -const actualDir = path.join(__dirname, `../../images/actual/${platform}`); -const expectedDir = path.join(__dirname, `../../maestro/images/expected/${platform}`); -const diffDir = path.join(__dirname, `../../images/diffs/${platform}-${process.env.GITHUB_RUN_ID}`); - -if (!fs.existsSync(diffDir)) { - fs.mkdirSync(diffDir, { recursive: true }); -} - -function isIgnored(x, y, ignoredAreas) { - return ignoredAreas.some(area => x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height); -} - -function applyIgnoredAreas(img, ignoredAreas) { - const { width, height } = img; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - if (isIgnored(x, y, ignoredAreas)) { - const idx = (width * y + x) << 2; - img.data[idx] = 0; // R - img.data[idx + 1] = 0; // G - img.data[idx + 2] = 0; // B - img.data[idx + 3] = 0; // A (transparent) - } - } - } -} - -const failedComparisons = []; - -fs.readdirSync(actualDir).forEach(file => { - const actualPath = path.join(actualDir, file); - const expectedPath = path.join(expectedDir, file); - const diffPath = path.join(diffDir, `diff_${file}`); - - if (fs.existsSync(expectedPath)) { - const actualImg = PNG.sync.read(fs.readFileSync(actualPath)); - const expectedImg = PNG.sync.read(fs.readFileSync(expectedPath)); - const { width, height } = actualImg; - const diff = new PNG({ width, height }); - - let ignoredAreas = []; - if (platform === "ios") { - ignoredAreas = [ - { x: 0, y: height - 40, width, height: 40 } // Ignore bottom 40 pixels where is Home Indicator on iOS - ]; - } else if (platform === "android") { - ignoredAreas = [ - { x: 0, y: 0, width, height: 50 }, // Ignore top 50 pixels on Android - { x: width - 15, y: 0, width: 15, height } // Ignore right 15 pixels on Android - ]; - } - - applyIgnoredAreas(actualImg, ignoredAreas); - applyIgnoredAreas(expectedImg, ignoredAreas); - - const numDiffPixels = pixelmatch(actualImg.data, expectedImg.data, diff.data, width, height, { - threshold: 0.1, - includeAA: false, - diffMask: true - }); - - const toleranceAmount = 0.02; // 2% - const totalPixels = width * height; - const pixelTolerance = Math.floor(totalPixels * toleranceAmount); - - if (numDiffPixels > pixelTolerance) { - fs.writeFileSync(diffPath, PNG.sync.write(diff)); - failedComparisons.push(file); - console.log(`❌ Comparison failed for ${file} (diff: ${numDiffPixels} > tolerance: ${pixelTolerance})`); - } else { - fs.appendFileSync(path.join(__dirname, "../../compare_output.txt"), `✅ Comparison passed for ${file}\n`); - console.log(`✅ Comparison passed for ${file} (diff: ${numDiffPixels} <= tolerance: ${pixelTolerance})`); - } - } else { - console.log(`⚠️ Expected file not found for ${file}`); - } -}); - -if (failedComparisons.length > 0) { - fs.appendFileSync(path.join(__dirname, "../../compare_output.txt"), `❌ Failed Comparisons:\n`); - failedComparisons.forEach(file => { - fs.appendFileSync(path.join(__dirname, "../../compare_output.txt"), ` - ${file}\n`); - }); - console.log(`❌ Failed Comparisons: ${failedComparisons.join(", ")}`); - core.exportVariable(`${platform.toUpperCase()}_COMPARISON_FAILED`, "true"); -} else { - fs.appendFileSync(path.join(__dirname, "../../compare_output.txt"), "All comparisons passed!\n"); - console.log("All comparisons passed!"); -} diff --git a/maestro/helpers/helpers.sh b/maestro/helpers/helpers.sh index 742855f5c..d035668e1 100644 --- a/maestro/helpers/helpers.sh +++ b/maestro/helpers/helpers.sh @@ -1,12 +1,89 @@ #!/bin/bash -MAX_RETRIES=3 +# One retry after the initial run (2 attempts total). Was 3, which combined with the +# initial run in run_tests meant up to 4 attempts per failing test — a genuinely broken +# build burned ~25 min instead of failing fast (S6: retry amplification). +MAX_RETRIES=1 RETRY_DELAY=10 +# Timeout for the Maestro driver (instrumentation) to come up. Maestro's default is 15s; +# CI emulators/simulators are slower, but 5 min (300000) was the per-attempt multiplier that +# amplified broken-build runs to ~25 min. 2 min is generous for the driver to start while +# keeping the worst-case failure time bounded. +MAESTRO_DRIVER_STARTUP_TIMEOUT=120000 + +# --- Per-flow video recording (BrowserStack-style debugging) ---------------------------- +# We record a screen video around every flow, then KEEP it only if the flow FAILS and DELETE +# it on pass — so artifact storage holds just the videos you actually need to debug, each a +# short clip of exactly the failing interaction (not one giant reel to scrub). +# iOS: `simctl io booted recordVideo` — runs until SIGINT, no length cap. SIGINT (not +# SIGKILL) so the trailing moov atom is written and the mp4 is playable. +# Android: `adb shell screenrecord` — caps at 180s/segment; a longer flow is truncated but +# still useful. Started AFTER `adb root`/status-bar setup so adbd restarts don't +# kill the recording mid-flow. +# Toggle with RECORD_VIDEO=false (default on) if recording ever worsens flake on the +# constrained CI runners. +RECORD_VIDEO="${RECORD_VIDEO:-true}" +VIDEO_DIR="${VIDEO_DIR:-$PWD/maestro/videos}" +ANDROID_REC_DEVICE_PATH="/sdcard/maestro-recording.mp4" +REC_PID="" +REC_FILE="" + +start_recording() { + [ "$RECORD_VIDEO" = "true" ] || return 0 + local label="$1" + REC_PID="" + mkdir -p "$VIDEO_DIR" + # Sanitize the label into a filename; one widget per shard so collisions are intra-flow only + # (a retry overwrites the first attempt's clip, which is what we want). + REC_FILE="$VIDEO_DIR/${PLATFORM}-$(echo "$label" | tr -c 'A-Za-z0-9._-' '_').mp4" + rm -f "$REC_FILE" + if [ "$PLATFORM" == "android" ]; then + adb shell screenrecord --bit-rate 4000000 --time-limit 180 "$ANDROID_REC_DEVICE_PATH" >/dev/null 2>&1 & + REC_PID=$! + else + xcrun simctl io booted recordVideo --codec h264 --force "$REC_FILE" >/dev/null 2>&1 & + REC_PID=$! + fi +} + +# stop_recording +stop_recording() { + [ "$RECORD_VIDEO" = "true" ] || return 0 + local keep="$1" + [ -z "$REC_PID" ] && return 0 + if [ "$PLATFORM" == "android" ]; then + # CRITICAL: stop the ON-DEVICE screenrecord, not just the local adb client. Killing only + # REC_PID (the `adb shell screenrecord` client) leaves the recorder running on the device + # until its --time-limit (180s), so `wait` would block ~2.5 min per flow and blow the job + # timeout. `pkill -INT` makes screenrecord finalize the mp4 (moov atom) promptly. + adb shell pkill -INT screenrecord >/dev/null 2>&1 || true + sleep 2 # let screenrecord write the trailer + flush to /sdcard + kill "$REC_PID" 2>/dev/null || true # client should already be gone; ensure it, never block + wait "$REC_PID" 2>/dev/null || true + if [ "$keep" == "keep" ]; then + adb pull "$ANDROID_REC_DEVICE_PATH" "$REC_FILE" >/dev/null 2>&1 || true + fi + adb shell rm -f "$ANDROID_REC_DEVICE_PATH" >/dev/null 2>&1 || true + else + # iOS: SIGINT lets simctl recordVideo finalize the file (moov atom); SIGKILL would corrupt it. + # recordVideo has no time-limit, so the client exits promptly on the signal. + kill -INT "$REC_PID" 2>/dev/null || true + wait "$REC_PID" 2>/dev/null || true + fi + REC_PID="" + if [ "$keep" != "keep" ]; then + rm -f "$REC_FILE" + fi + REC_FILE="" +} + # Function to restart the iOS simulator restart_simulator() { echo "🔄 Restarting iOS Simulator..." - xcrun simctl shutdown "$IOS_DEVICE" + # Shut down whatever is booted; the device is auto-selected in prepare_ios.sh, + # so we don't depend on a hardcoded device name here. + xcrun simctl shutdown all || true sleep 10 bash ./maestro/helpers/prepare_ios.sh } @@ -67,11 +144,14 @@ run_tests() { ensure_emulator_ready set_status_bar fi - if $HOME/.local/bin/maestro/bin/maestro test --env APP_ID=$APP_ID --env PLATFORM=$PLATFORM --env MAESTRO_DRIVER_STARTUP_TIMEOUT=300000 "$yaml_test_file"; then + start_recording "$(basename "${yaml_test_file%.yaml}")" + if $HOME/.local/bin/maestro/bin/maestro test --env APP_ID=$APP_ID --env PLATFORM=$PLATFORM --env MAESTRO_DRIVER_STARTUP_TIMEOUT=$MAESTRO_DRIVER_STARTUP_TIMEOUT "$yaml_test_file"; then echo "✅ Test passed: $yaml_test_file" + stop_recording discard passed_tests+=("$yaml_test_file") else echo "❌ Test failed: $yaml_test_file" + stop_recording keep failed_tests+=("$yaml_test_file") fi completed_tests=$((completed_tests + 1)) @@ -80,6 +160,49 @@ run_tests() { done } +# Fast-fail smoke check (S6): verify the app launches and the Widgets menu renders ONCE, +# before running any widget flows. If it fails the build is fundamentally broken, so abort +# the shard immediately rather than burning every flow × retries up to the job timeout. +smoke_check() { + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + # 2 attempts. The first failure is usually a transient Maestro driver/simulator attach + # flake (e.g. iOS "Unable to set permissions … Failed to connect to 127.0.0.1:") + # that kills an otherwise-healthy shard. We restart the driver/sim and retry ONCE — a + # genuinely broken build still fails fast (~2 short attempts), so the fast-fail intent holds. + local max_attempts=2 + local attempt=1 + while [ "$attempt" -le "$max_attempts" ]; do + echo "🔎 Smoke check (attempt $attempt/$max_attempts): app launches and 'Widgets menu' renders?" + if [ "$PLATFORM" == "android" ]; then + ensure_emulator_ready + set_status_bar + fi + start_recording "smoke-attempt-$attempt" + if $HOME/.local/bin/maestro/bin/maestro test --env APP_ID=$APP_ID --env PLATFORM=$PLATFORM --env MAESTRO_DRIVER_STARTUP_TIMEOUT=$MAESTRO_DRIVER_STARTUP_TIMEOUT "$script_dir/Smoke.yaml"; then + echo "✅ Smoke check passed — running widget flows." + stop_recording discard + return 0 + fi + # Keep this attempt's clip — a launch crash / blank screen here is exactly what we want to see. + stop_recording keep + if [ "$attempt" -lt "$max_attempts" ]; then + echo "⚠️ Smoke check attempt $attempt failed — resetting driver/simulator and retrying once." + # Reset the layer that actually flakes: on iOS restart the sim (re-runs prepare_ios.sh, + # re-establishing the Maestro driver); on Android just re-confirm the emulator is up. + if [ "$PLATFORM" == "android" ]; then + ensure_emulator_ready + else + restart_simulator + fi + fi + attempt=$((attempt + 1)) + done + echo "❌ Smoke check FAILED after $max_attempts attempts — app did not launch / 'Widgets menu' never rendered." + echo " Build/bundle is likely broken; aborting shard fast instead of retrying every flow." + return 1 +} + # Function to rerun failed tests rerun_failed_tests() { local retry_failed_tests=("$@") @@ -95,12 +218,15 @@ rerun_failed_tests() { fi local attempt=0 while [ $attempt -lt $MAX_RETRIES ]; do - if $HOME/.local/bin/maestro/bin/maestro test --env APP_ID=$APP_ID --env PLATFORM=$PLATFORM --env MAESTRO_DRIVER_STARTUP_TIMEOUT=300000 "$yaml_test_file"; then + start_recording "$(basename "${yaml_test_file%.yaml}")" + if $HOME/.local/bin/maestro/bin/maestro test --env APP_ID=$APP_ID --env PLATFORM=$PLATFORM --env MAESTRO_DRIVER_STARTUP_TIMEOUT=$MAESTRO_DRIVER_STARTUP_TIMEOUT "$yaml_test_file"; then echo "✅ Test passed: $yaml_test_file" + stop_recording discard passed_tests+=("$yaml_test_file") break else echo "❌ Test failed: $yaml_test_file (Attempt $((attempt + 1))/$MAX_RETRIES)" + stop_recording keep attempt=$((attempt + 1)) if [ $attempt -lt $MAX_RETRIES ]; then echo "Retrying in $RETRY_DELAY seconds..." diff --git a/maestro/helpers/prepare_android.sh b/maestro/helpers/prepare_android.sh index a542b3d15..478a5df9c 100644 --- a/maestro/helpers/prepare_android.sh +++ b/maestro/helpers/prepare_android.sh @@ -3,10 +3,27 @@ MAX_RETRIES=5 RETRY_DELAY=10 RETRIES=0 +BOOT_TIMEOUT=300 + +# Wait until the emulator is actually ready instead of guessing with a fixed sleep: +# device attached -> sys.boot_completed=1 -> package manager responds. Bounded so a +# never-booting emulator fails fast instead of hanging. +wait_for_emulator() { + echo "Waiting for emulator to be ready..." + adb wait-for-device + local deadline=$(( $(date +%s) + BOOT_TIMEOUT )) + until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] \ + && adb shell pm list packages >/dev/null 2>&1; do + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "Emulator not ready after ${BOOT_TIMEOUT}s" + exit 1 + fi + sleep 2 + done + echo "Emulator is ready." +} -# Add a delay to ensure the emulator is fully booted -echo "Waiting for emulator to be ready..." -sleep 30 +wait_for_emulator # Function to install the Android app on the emulator install_android_app() { diff --git a/maestro/helpers/prepare_ios.sh b/maestro/helpers/prepare_ios.sh index 87f639cf5..c914b9a4f 100644 --- a/maestro/helpers/prepare_ios.sh +++ b/maestro/helpers/prepare_ios.sh @@ -1,45 +1,67 @@ #!/bin/bash -# Configuration - modify these values as needed -DEVICE_TYPE="iPhone 16" -IOS_VERSION="18.5" +# Screenshot baselines are device/resolution-specific, so prefer a PINNED device for +# reproducible visual regression. If that exact device isn't on the runner image (e.g. a +# future macos-NN bump drops/renames it), fall back to the newest available iPhone and warn +# loudly that baselines likely need regenerating — degrade gracefully instead of hard-failing +# like the old hardcoded device did. Override the pin via PREFERRED_IOS_DEVICE. +# Exported (not just assigned) so the python3 process in the pipeline below inherits it. +# A `VAR=x cmd | python3` prefix only applies to `cmd`, NOT to python3 (separate pipeline +# process), so without `export` python sees an empty preferred name and always falls back. +export PREFERRED_IOS_DEVICE="${PREFERRED_IOS_DEVICE:-iPhone 17 Pro}" start_simulator() { - echo "Starting iOS Simulator..." - echo "Looking for: $DEVICE_TYPE with iOS $IOS_VERSION" - - # List available simulators to debug - # echo "Available simulators:" - # xcrun simctl list devices available - - # Try to find the specified device with the specified iOS version - DEVICE_ID=$(xcrun simctl list devices available | grep -A1 "iOS $IOS_VERSION" | grep "$DEVICE_TYPE " | head -1 | grep -o "[A-F0-9-]{36}") - - if [ -z "$DEVICE_ID" ]; then - echo "No $DEVICE_TYPE with iOS $IOS_VERSION found. Trying to create one..." - # Try to create the device with the specified iOS version - IOS_RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-${IOS_VERSION//./-}" - DEVICE_TYPE_ID="com.apple.CoreSimulator.SimDeviceType.${DEVICE_TYPE// /-}" - DEVICE_ID=$(xcrun simctl create "${DEVICE_TYPE} Test" "$DEVICE_TYPE_ID" "$IOS_RUNTIME" 2>/dev/null) - - if [ -z "$DEVICE_ID" ]; then - echo "Failed to create $DEVICE_TYPE with iOS $IOS_VERSION. Using any available $DEVICE_TYPE." - DEVICE_ID=$(xcrun simctl list devices available | grep "$DEVICE_TYPE " | head -1 | grep -o "[A-F0-9-]{36}") - fi - fi - + echo "Selecting iOS simulator (preferred: '$PREFERRED_IOS_DEVICE')..." + + # Parse `simctl list` JSON. Output is TAB-delimited (device names contain spaces) as: + # \t\t\t where status is "pinned" or "fallback". + IFS=$'\t' read -r DEVICE_ID DEVICE_NAME RUNTIME_VER SELECT_STATUS < <( + xcrun simctl list -j devices available | python3 -c ' +import json, os, sys, re +preferred = os.environ.get("PREFERRED_IOS_DEVICE", "") +data = json.load(sys.stdin).get("devices", {}) +best = None # newest available iPhone overall (fallback) +pinned = None # highest iOS runtime for the preferred device name +for runtime, devices in data.items(): + m = re.search(r"iOS-(\d+)-(\d+)", runtime) + if not m: + continue + ios_ver = (int(m.group(1)), int(m.group(2))) + for d in devices: + if not d.get("isAvailable", False): + continue + name = d.get("name", "") + if not name.startswith("iPhone"): + continue + if name == preferred and (pinned is None or ios_ver > pinned[0]): + pinned = (ios_ver, d["udid"], name) + mm = re.search(r"iPhone (\d+)", name) + model = int(mm.group(1)) if mm else 0 + key = (ios_ver, model) + if best is None or key > best[0]: + best = (key, d["udid"], name, ios_ver) +if pinned: + print("\t".join([pinned[1], pinned[2], f"{pinned[0][0]}.{pinned[0][1]}", "pinned"])) +elif best: + print("\t".join([best[1], best[2], f"{best[3][0]}.{best[3][1]}", "fallback"])) +' + ) + if [ -z "$DEVICE_ID" ]; then - echo "Error: Could not find or create any $DEVICE_TYPE device" + echo "Error: no available iPhone simulator found" + xcrun simctl list devices available || true exit 1 fi - - echo "Using device ID: $DEVICE_ID" + + if [ "$SELECT_STATUS" = "fallback" ]; then + echo "::warning::Pinned iOS device '$PREFERRED_IOS_DEVICE' not available; using newest '$DEVICE_NAME' (iOS $RUNTIME_VER). Screenshot baselines may differ — regenerate with update_baselines=true if visual asserts fail." + fi + + echo "Using $DEVICE_NAME (iOS $RUNTIME_VER) [$SELECT_STATUS], UDID: $DEVICE_ID" export SIMULATOR_DEVICE_ID="$DEVICE_ID" - - # Boot the device + xcrun simctl boot "$DEVICE_ID" || echo "Simulator already booted" - sleep 30 - xcrun simctl bootstatus "$DEVICE_ID" || echo "Simulator booted successfully" + xcrun simctl bootstatus "$DEVICE_ID" -b || echo "Simulator booted" } set_status_bar() { diff --git a/maestro/helpers/update_baselines.sh b/maestro/helpers/update_baselines.sh new file mode 100755 index 000000000..47bd61ed7 --- /dev/null +++ b/maestro/helpers/update_baselines.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Refresh the committed assertScreenshot baselines from a capture run. +# +# Prerequisite: trigger a capture run that produces (but does not assert) screenshots: +# gh workflow run NativePipeline.yml --ref -f update_baselines=true +# When it finishes, run this script to pull the *-screenshots-* artifacts and drop them +# into maestro/images/expected/{android,ios}/, then review + commit. +# +# Usage: +# maestro/helpers/update_baselines.sh [RUN_ID] +# +# RUN_ID GitHub Actions run id to download from. Defaults to the most recent +# NativePipeline run on the current branch. +set -euo pipefail + +command -v gh >/dev/null 2>&1 || { echo "❌ gh CLI is required"; exit 1; } + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +expected_root="$repo_root/maestro/images/expected" + +run_id="${1:-}" +if [ -z "$run_id" ]; then + branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD)" + echo "ℹ️ No run id given; using the latest NativePipeline run on '$branch'." + run_id="$(gh run list --workflow NativePipeline.yml --branch "$branch" -L1 \ + --json databaseId -q '.[0].databaseId')" + [ -n "$run_id" ] || { echo "❌ Could not find a run on '$branch'"; exit 1; } +fi +echo "📥 Downloading screenshot artifacts from run $run_id ..." + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +# Pull every per-widget / per-jsactions screenshot artifact (android-screenshots-*, ios-screenshots-*). +gh run download "$run_id" --pattern '*-screenshots-*' --dir "$tmp" \ + || { echo "❌ No screenshot artifacts found on run $run_id"; exit 1; } + +total=0 +for platform in android ios; do + dest="$expected_root/$platform" + mkdir -p "$dest" + # Artifacts are named "-screenshots-"; copy every PNG inside, keyed by + # basename (takeScreenshot writes flat .png, so basename uniquely identifies a baseline). + count=0 + while IFS= read -r -d '' png; do + cp -f "$png" "$dest/$(basename "$png")" + count=$((count + 1)) + done < <(find "$tmp" -type d -name "${platform}-screenshots-*" -exec find {} -name '*.png' -print0 \;) + echo " $platform: $count screenshots → $dest" + total=$((total + count)) +done + +[ "$total" -gt 0 ] || { echo "❌ No PNGs found in the downloaded artifacts"; exit 1; } + +echo +echo "✅ Updated $total baselines. Review and commit:" +echo " git add maestro/images/expected && git status --short maestro/images/expected" diff --git a/maestro/images/expected/android/SafeAreaViewMaps.png b/maestro/images/expected/android/SafeAreaViewMaps.png deleted file mode 100644 index 09328e739..000000000 Binary files a/maestro/images/expected/android/SafeAreaViewMaps.png and /dev/null differ diff --git a/maestro/images/expected/android/accordion_custom.png b/maestro/images/expected/android/accordion_custom.png index 9c2232912..5ba1cd602 100644 Binary files a/maestro/images/expected/android/accordion_custom.png and b/maestro/images/expected/android/accordion_custom.png differ diff --git a/maestro/images/expected/android/accordion_multiple.png b/maestro/images/expected/android/accordion_multiple.png index aa75378a6..51defc978 100644 Binary files a/maestro/images/expected/android/accordion_multiple.png and b/maestro/images/expected/android/accordion_multiple.png differ diff --git a/maestro/images/expected/android/accordion_noncollapsible.png b/maestro/images/expected/android/accordion_noncollapsible.png index 3ced2fb4c..635cc8051 100644 Binary files a/maestro/images/expected/android/accordion_noncollapsible.png and b/maestro/images/expected/android/accordion_noncollapsible.png differ diff --git a/maestro/images/expected/android/activity_indicator.png b/maestro/images/expected/android/activity_indicator.png deleted file mode 100644 index 8a1f98f16..000000000 Binary files a/maestro/images/expected/android/activity_indicator.png and /dev/null differ diff --git a/maestro/images/expected/android/animation.png b/maestro/images/expected/android/animation.png index 69fde3a6c..c8ad13956 100644 Binary files a/maestro/images/expected/android/animation.png and b/maestro/images/expected/android/animation.png differ diff --git a/maestro/images/expected/android/authentication.png b/maestro/images/expected/android/authentication.png index da2c54511..c10d61b82 100644 Binary files a/maestro/images/expected/android/authentication.png and b/maestro/images/expected/android/authentication.png differ diff --git a/maestro/images/expected/android/bar_chart.png b/maestro/images/expected/android/bar_chart.png index ec30f2ad8..6646c1770 100644 Binary files a/maestro/images/expected/android/bar_chart.png and b/maestro/images/expected/android/bar_chart.png differ diff --git a/maestro/images/expected/android/bg_image_clickable_container.png b/maestro/images/expected/android/bg_image_clickable_container.png index 3b236773c..8b5cf7e9d 100644 Binary files a/maestro/images/expected/android/bg_image_clickable_container.png and b/maestro/images/expected/android/bg_image_clickable_container.png differ diff --git a/maestro/images/expected/android/bg_image_conditional_visibility.png b/maestro/images/expected/android/bg_image_conditional_visibility.png index 99455bed3..17577bf57 100644 Binary files a/maestro/images/expected/android/bg_image_conditional_visibility.png and b/maestro/images/expected/android/bg_image_conditional_visibility.png differ diff --git a/maestro/images/expected/android/bg_image_dynamic_image.png b/maestro/images/expected/android/bg_image_dynamic_image.png index e60cea4b4..0bf3c893f 100644 Binary files a/maestro/images/expected/android/bg_image_dynamic_image.png and b/maestro/images/expected/android/bg_image_dynamic_image.png differ diff --git a/maestro/images/expected/android/bg_image_dynamic_svg.png b/maestro/images/expected/android/bg_image_dynamic_svg.png index ab6aaad57..ebd38ac4c 100644 Binary files a/maestro/images/expected/android/bg_image_dynamic_svg.png and b/maestro/images/expected/android/bg_image_dynamic_svg.png differ diff --git a/maestro/images/expected/android/bg_image_layout_grid.png b/maestro/images/expected/android/bg_image_layout_grid.png index 5affc44f2..f120e8fd6 100644 Binary files a/maestro/images/expected/android/bg_image_layout_grid.png and b/maestro/images/expected/android/bg_image_layout_grid.png differ diff --git a/maestro/images/expected/android/bg_image_nested.png b/maestro/images/expected/android/bg_image_nested.png index cd70758b1..29627fe5f 100644 Binary files a/maestro/images/expected/android/bg_image_nested.png and b/maestro/images/expected/android/bg_image_nested.png differ diff --git a/maestro/images/expected/android/bg_image_static_images.png b/maestro/images/expected/android/bg_image_static_images.png index 376ec8bb9..d6e21f3d1 100644 Binary files a/maestro/images/expected/android/bg_image_static_images.png and b/maestro/images/expected/android/bg_image_static_images.png differ diff --git a/maestro/images/expected/android/bg_image_static_svg.png b/maestro/images/expected/android/bg_image_static_svg.png index dc9a4b965..31c427295 100644 Binary files a/maestro/images/expected/android/bg_image_static_svg.png and b/maestro/images/expected/android/bg_image_static_svg.png differ diff --git a/maestro/images/expected/android/bottom_sheet_expanding.png b/maestro/images/expected/android/bottom_sheet_expanding.png index 472702c32..6d6326037 100644 Binary files a/maestro/images/expected/android/bottom_sheet_expanding.png and b/maestro/images/expected/android/bottom_sheet_expanding.png differ diff --git a/maestro/images/expected/android/bottom_sheet_expanding_fullscreen.png b/maestro/images/expected/android/bottom_sheet_expanding_fullscreen.png index 650086465..93c58bcc0 100644 Binary files a/maestro/images/expected/android/bottom_sheet_expanding_fullscreen.png and b/maestro/images/expected/android/bottom_sheet_expanding_fullscreen.png differ diff --git a/maestro/images/expected/android/bottom_sheet_modal_basic_non_native.png b/maestro/images/expected/android/bottom_sheet_modal_basic_non_native.png index 4e1d3be77..c599cd724 100644 Binary files a/maestro/images/expected/android/bottom_sheet_modal_basic_non_native.png and b/maestro/images/expected/android/bottom_sheet_modal_basic_non_native.png differ diff --git a/maestro/images/expected/android/bottom_sheet_modal_custom.png b/maestro/images/expected/android/bottom_sheet_modal_custom.png index 919a6de14..392e2431c 100644 Binary files a/maestro/images/expected/android/bottom_sheet_modal_custom.png and b/maestro/images/expected/android/bottom_sheet_modal_custom.png differ diff --git a/maestro/images/expected/android/clipboard.png b/maestro/images/expected/android/clipboard.png index 9ac82d3f1..9992b5e6a 100644 Binary files a/maestro/images/expected/android/clipboard.png and b/maestro/images/expected/android/clipboard.png differ diff --git a/maestro/images/expected/android/color_picker_conditional.png b/maestro/images/expected/android/color_picker_conditional.png index 81d0f2783..e6e8acc09 100644 Binary files a/maestro/images/expected/android/color_picker_conditional.png and b/maestro/images/expected/android/color_picker_conditional.png differ diff --git a/maestro/images/expected/android/color_picker_disabled.png b/maestro/images/expected/android/color_picker_disabled.png index 91b1e4e64..d57739fae 100644 Binary files a/maestro/images/expected/android/color_picker_disabled.png and b/maestro/images/expected/android/color_picker_disabled.png differ diff --git a/maestro/images/expected/android/color_picker_normal.png b/maestro/images/expected/android/color_picker_normal.png index 6f0927d2f..62957b87b 100644 Binary files a/maestro/images/expected/android/color_picker_normal.png and b/maestro/images/expected/android/color_picker_normal.png differ diff --git a/maestro/images/expected/android/color_picker_partial.png b/maestro/images/expected/android/color_picker_partial.png index 3928a935c..bb449210a 100644 Binary files a/maestro/images/expected/android/color_picker_partial.png and b/maestro/images/expected/android/color_picker_partial.png differ diff --git a/maestro/images/expected/android/column_chart.png b/maestro/images/expected/android/column_chart.png index 935316a4c..c85c3c368 100644 Binary files a/maestro/images/expected/android/column_chart.png and b/maestro/images/expected/android/column_chart.png differ diff --git a/maestro/images/expected/android/device_info.png b/maestro/images/expected/android/device_info.png index 2391fbd55..a9b029be8 100644 Binary files a/maestro/images/expected/android/device_info.png and b/maestro/images/expected/android/device_info.png differ diff --git a/maestro/images/expected/android/doughnut_chart_custom.png b/maestro/images/expected/android/doughnut_chart_custom.png index a7c8776bc..3cbf84901 100644 Binary files a/maestro/images/expected/android/doughnut_chart_custom.png and b/maestro/images/expected/android/doughnut_chart_custom.png differ diff --git a/maestro/images/expected/android/doughnut_chart_multiple_data_points.png b/maestro/images/expected/android/doughnut_chart_multiple_data_points.png index e1d588631..afd5ff0ee 100644 Binary files a/maestro/images/expected/android/doughnut_chart_multiple_data_points.png and b/maestro/images/expected/android/doughnut_chart_multiple_data_points.png differ diff --git a/maestro/images/expected/android/floating_action_button.png b/maestro/images/expected/android/floating_action_button.png index e0b82aa64..8d1932e0b 100644 Binary files a/maestro/images/expected/android/floating_action_button.png and b/maestro/images/expected/android/floating_action_button.png differ diff --git a/maestro/images/expected/android/image_dynamic.png b/maestro/images/expected/android/image_dynamic.png index 88031dc22..fe3a577be 100644 Binary files a/maestro/images/expected/android/image_dynamic.png and b/maestro/images/expected/android/image_dynamic.png differ diff --git a/maestro/images/expected/android/image_icon.png b/maestro/images/expected/android/image_icon.png index 7d8fea92f..3abf2e299 100644 Binary files a/maestro/images/expected/android/image_icon.png and b/maestro/images/expected/android/image_icon.png differ diff --git a/maestro/images/expected/android/image_static.png b/maestro/images/expected/android/image_static.png index 9499064f8..6df462a01 100644 Binary files a/maestro/images/expected/android/image_static.png and b/maestro/images/expected/android/image_static.png differ diff --git a/maestro/images/expected/android/image_url.png b/maestro/images/expected/android/image_url.png index 298349b73..84137e764 100644 Binary files a/maestro/images/expected/android/image_url.png and b/maestro/images/expected/android/image_url.png differ diff --git a/maestro/images/expected/android/line_chart.png b/maestro/images/expected/android/line_chart.png index d1078c00a..67ad65ce4 100644 Binary files a/maestro/images/expected/android/line_chart.png and b/maestro/images/expected/android/line_chart.png differ diff --git a/maestro/images/expected/android/pie_chart_custom.png b/maestro/images/expected/android/pie_chart_custom.png index ba713a628..137433a41 100644 Binary files a/maestro/images/expected/android/pie_chart_custom.png and b/maestro/images/expected/android/pie_chart_custom.png differ diff --git a/maestro/images/expected/android/pie_chart_multiple_data_points.png b/maestro/images/expected/android/pie_chart_multiple_data_points.png index 196807000..2d4c8e89e 100644 Binary files a/maestro/images/expected/android/pie_chart_multiple_data_points.png and b/maestro/images/expected/android/pie_chart_multiple_data_points.png differ diff --git a/maestro/images/expected/android/popup_menu_alert.png b/maestro/images/expected/android/popup_menu_alert.png index a48f6bed5..9573614d8 100644 Binary files a/maestro/images/expected/android/popup_menu_alert.png and b/maestro/images/expected/android/popup_menu_alert.png differ diff --git a/maestro/images/expected/android/progress_bar.png b/maestro/images/expected/android/progress_bar.png index 8bc7985cc..0dff3f555 100644 Binary files a/maestro/images/expected/android/progress_bar.png and b/maestro/images/expected/android/progress_bar.png differ diff --git a/maestro/images/expected/android/progress_circle.png b/maestro/images/expected/android/progress_circle.png index ec2fd3f89..fc6992fd5 100644 Binary files a/maestro/images/expected/android/progress_circle.png and b/maestro/images/expected/android/progress_circle.png differ diff --git a/maestro/images/expected/android/qr_code.png b/maestro/images/expected/android/qr_code.png index 1147e57d2..933edda6e 100644 Binary files a/maestro/images/expected/android/qr_code.png and b/maestro/images/expected/android/qr_code.png differ diff --git a/maestro/images/expected/android/radio_buttons.png b/maestro/images/expected/android/radio_buttons.png index 0dc485000..1264b28c5 100644 Binary files a/maestro/images/expected/android/radio_buttons.png and b/maestro/images/expected/android/radio_buttons.png differ diff --git a/maestro/images/expected/android/range_slider.png b/maestro/images/expected/android/range_slider.png index 2287617c2..f5711b779 100644 Binary files a/maestro/images/expected/android/range_slider.png and b/maestro/images/expected/android/range_slider.png differ diff --git a/maestro/images/expected/android/rating.png b/maestro/images/expected/android/rating.png index 6f3002d40..3b65b4a04 100644 Binary files a/maestro/images/expected/android/rating.png and b/maestro/images/expected/android/rating.png differ diff --git a/maestro/images/expected/android/safe_area_view_bottom_bar.png b/maestro/images/expected/android/safe_area_view_bottom_bar.png index c1b9360ed..d6fb1f9c9 100644 Binary files a/maestro/images/expected/android/safe_area_view_bottom_bar.png and b/maestro/images/expected/android/safe_area_view_bottom_bar.png differ diff --git a/maestro/images/expected/android/safe_area_view_image.png b/maestro/images/expected/android/safe_area_view_image.png index d1be50de8..7438b8f1c 100644 Binary files a/maestro/images/expected/android/safe_area_view_image.png and b/maestro/images/expected/android/safe_area_view_image.png differ diff --git a/maestro/images/expected/android/safe_area_view_text.png b/maestro/images/expected/android/safe_area_view_text.png index 661a8b482..108ca53e3 100644 Binary files a/maestro/images/expected/android/safe_area_view_text.png and b/maestro/images/expected/android/safe_area_view_text.png differ diff --git a/maestro/images/expected/android/safe_area_view_top_bar.png b/maestro/images/expected/android/safe_area_view_top_bar.png index e6c219432..79c52993a 100644 Binary files a/maestro/images/expected/android/safe_area_view_top_bar.png and b/maestro/images/expected/android/safe_area_view_top_bar.png differ diff --git a/maestro/images/expected/android/slider.png b/maestro/images/expected/android/slider.png index 21d15a1af..a60624cc8 100644 Binary files a/maestro/images/expected/android/slider.png and b/maestro/images/expected/android/slider.png differ diff --git a/maestro/images/expected/android/toggle_buttons.png b/maestro/images/expected/android/toggle_buttons.png index b5639ef06..d87b5e0b3 100644 Binary files a/maestro/images/expected/android/toggle_buttons.png and b/maestro/images/expected/android/toggle_buttons.png differ diff --git a/maestro/images/expected/android/toggle_sidebar.png b/maestro/images/expected/android/toggle_sidebar.png index 6aec5f577..2d1da85ce 100644 Binary files a/maestro/images/expected/android/toggle_sidebar.png and b/maestro/images/expected/android/toggle_sidebar.png differ diff --git a/maestro/images/expected/ios/accordion_custom.png b/maestro/images/expected/ios/accordion_custom.png index 62babe9a3..d2784132e 100644 Binary files a/maestro/images/expected/ios/accordion_custom.png and b/maestro/images/expected/ios/accordion_custom.png differ diff --git a/maestro/images/expected/ios/accordion_multiple.png b/maestro/images/expected/ios/accordion_multiple.png index c9de7ca90..6fd7b49e9 100644 Binary files a/maestro/images/expected/ios/accordion_multiple.png and b/maestro/images/expected/ios/accordion_multiple.png differ diff --git a/maestro/images/expected/ios/accordion_noncollapsible.png b/maestro/images/expected/ios/accordion_noncollapsible.png index ac607d44a..746f6f4a3 100644 Binary files a/maestro/images/expected/ios/accordion_noncollapsible.png and b/maestro/images/expected/ios/accordion_noncollapsible.png differ diff --git a/maestro/images/expected/ios/accordion_on_change.png b/maestro/images/expected/ios/accordion_on_change.png deleted file mode 100644 index 100d75542..000000000 Binary files a/maestro/images/expected/ios/accordion_on_change.png and /dev/null differ diff --git a/maestro/images/expected/ios/activity_indicator.png b/maestro/images/expected/ios/activity_indicator.png deleted file mode 100644 index 49f270f6c..000000000 Binary files a/maestro/images/expected/ios/activity_indicator.png and /dev/null differ diff --git a/maestro/images/expected/ios/animation.png b/maestro/images/expected/ios/animation.png index 9e98ac5a0..d3ac43465 100644 Binary files a/maestro/images/expected/ios/animation.png and b/maestro/images/expected/ios/animation.png differ diff --git a/maestro/images/expected/ios/authentication.png b/maestro/images/expected/ios/authentication.png index 4d01f5f1f..3253a01ab 100644 Binary files a/maestro/images/expected/ios/authentication.png and b/maestro/images/expected/ios/authentication.png differ diff --git a/maestro/images/expected/ios/background_gradient.png b/maestro/images/expected/ios/background_gradient.png index f7619e9ea..f05ef055f 100644 Binary files a/maestro/images/expected/ios/background_gradient.png and b/maestro/images/expected/ios/background_gradient.png differ diff --git a/maestro/images/expected/ios/bar_chart.png b/maestro/images/expected/ios/bar_chart.png index f75ca5976..10b917c6e 100644 Binary files a/maestro/images/expected/ios/bar_chart.png and b/maestro/images/expected/ios/bar_chart.png differ diff --git a/maestro/images/expected/ios/bottom_sheet_expanding.png b/maestro/images/expected/ios/bottom_sheet_expanding.png index ef3a6072f..42b183efc 100644 Binary files a/maestro/images/expected/ios/bottom_sheet_expanding.png and b/maestro/images/expected/ios/bottom_sheet_expanding.png differ diff --git a/maestro/images/expected/ios/bottom_sheet_expanding_fullscreen.png b/maestro/images/expected/ios/bottom_sheet_expanding_fullscreen.png index 12481a2c9..0b796bff1 100644 Binary files a/maestro/images/expected/ios/bottom_sheet_expanding_fullscreen.png and b/maestro/images/expected/ios/bottom_sheet_expanding_fullscreen.png differ diff --git a/maestro/images/expected/ios/bottom_sheet_modal_basic_non_native.png b/maestro/images/expected/ios/bottom_sheet_modal_basic_non_native.png index e7035e7b7..0e221d087 100644 Binary files a/maestro/images/expected/ios/bottom_sheet_modal_basic_non_native.png and b/maestro/images/expected/ios/bottom_sheet_modal_basic_non_native.png differ diff --git a/maestro/images/expected/ios/bottom_sheet_modal_custom.png b/maestro/images/expected/ios/bottom_sheet_modal_custom.png index 3ebf71660..42d2d0fb8 100644 Binary files a/maestro/images/expected/ios/bottom_sheet_modal_custom.png and b/maestro/images/expected/ios/bottom_sheet_modal_custom.png differ diff --git a/maestro/images/expected/ios/clipboard.png b/maestro/images/expected/ios/clipboard.png index fb19225fd..79df77d58 100644 Binary files a/maestro/images/expected/ios/clipboard.png and b/maestro/images/expected/ios/clipboard.png differ diff --git a/maestro/images/expected/ios/color_picker_conditional.png b/maestro/images/expected/ios/color_picker_conditional.png index 94086c101..6d406959e 100644 Binary files a/maestro/images/expected/ios/color_picker_conditional.png and b/maestro/images/expected/ios/color_picker_conditional.png differ diff --git a/maestro/images/expected/ios/color_picker_disabled.png b/maestro/images/expected/ios/color_picker_disabled.png index 0fd0481e2..ba9034a67 100644 Binary files a/maestro/images/expected/ios/color_picker_disabled.png and b/maestro/images/expected/ios/color_picker_disabled.png differ diff --git a/maestro/images/expected/ios/color_picker_normal.png b/maestro/images/expected/ios/color_picker_normal.png index c655753b0..a8ffccd14 100644 Binary files a/maestro/images/expected/ios/color_picker_normal.png and b/maestro/images/expected/ios/color_picker_normal.png differ diff --git a/maestro/images/expected/ios/color_picker_partial.png b/maestro/images/expected/ios/color_picker_partial.png index faccfdb7e..9650f365c 100644 Binary files a/maestro/images/expected/ios/color_picker_partial.png and b/maestro/images/expected/ios/color_picker_partial.png differ diff --git a/maestro/images/expected/ios/column_chart.png b/maestro/images/expected/ios/column_chart.png index a77138968..1ecb248f9 100644 Binary files a/maestro/images/expected/ios/column_chart.png and b/maestro/images/expected/ios/column_chart.png differ diff --git a/maestro/images/expected/ios/device_info.png b/maestro/images/expected/ios/device_info.png new file mode 100644 index 000000000..f8c0ae6fc Binary files /dev/null and b/maestro/images/expected/ios/device_info.png differ diff --git a/maestro/images/expected/ios/doughnut_chart_custom.png b/maestro/images/expected/ios/doughnut_chart_custom.png index 40e0df43e..5aa2fe628 100644 Binary files a/maestro/images/expected/ios/doughnut_chart_custom.png and b/maestro/images/expected/ios/doughnut_chart_custom.png differ diff --git a/maestro/images/expected/ios/doughnut_chart_multiple_data_points.png b/maestro/images/expected/ios/doughnut_chart_multiple_data_points.png index 412618495..35e13427e 100644 Binary files a/maestro/images/expected/ios/doughnut_chart_multiple_data_points.png and b/maestro/images/expected/ios/doughnut_chart_multiple_data_points.png differ diff --git a/maestro/images/expected/ios/floating_action_button.png b/maestro/images/expected/ios/floating_action_button.png index caaebfc4b..afc0ac2be 100644 Binary files a/maestro/images/expected/ios/floating_action_button.png and b/maestro/images/expected/ios/floating_action_button.png differ diff --git a/maestro/images/expected/ios/image_dynamic.png b/maestro/images/expected/ios/image_dynamic.png index 513522c01..60931d032 100644 Binary files a/maestro/images/expected/ios/image_dynamic.png and b/maestro/images/expected/ios/image_dynamic.png differ diff --git a/maestro/images/expected/ios/image_icon.png b/maestro/images/expected/ios/image_icon.png index ad6180edb..28eabe90c 100644 Binary files a/maestro/images/expected/ios/image_icon.png and b/maestro/images/expected/ios/image_icon.png differ diff --git a/maestro/images/expected/ios/image_static.png b/maestro/images/expected/ios/image_static.png index 8db32f26a..90a7ef5d4 100644 Binary files a/maestro/images/expected/ios/image_static.png and b/maestro/images/expected/ios/image_static.png differ diff --git a/maestro/images/expected/ios/image_url.png b/maestro/images/expected/ios/image_url.png index f390f3ec4..80ec61ab9 100644 Binary files a/maestro/images/expected/ios/image_url.png and b/maestro/images/expected/ios/image_url.png differ diff --git a/maestro/images/expected/ios/line_chart.png b/maestro/images/expected/ios/line_chart.png index 73f106860..c48338521 100644 Binary files a/maestro/images/expected/ios/line_chart.png and b/maestro/images/expected/ios/line_chart.png differ diff --git a/maestro/images/expected/ios/pie_chart_custom.png b/maestro/images/expected/ios/pie_chart_custom.png index e5b34fe4e..8e4dddc5f 100644 Binary files a/maestro/images/expected/ios/pie_chart_custom.png and b/maestro/images/expected/ios/pie_chart_custom.png differ diff --git a/maestro/images/expected/ios/pie_chart_multiple_data_points.png b/maestro/images/expected/ios/pie_chart_multiple_data_points.png index 14069c299..498268b1c 100644 Binary files a/maestro/images/expected/ios/pie_chart_multiple_data_points.png and b/maestro/images/expected/ios/pie_chart_multiple_data_points.png differ diff --git a/maestro/images/expected/ios/popup_menu_alert.png b/maestro/images/expected/ios/popup_menu_alert.png index 0bc3ef06a..d11813e32 100644 Binary files a/maestro/images/expected/ios/popup_menu_alert.png and b/maestro/images/expected/ios/popup_menu_alert.png differ diff --git a/maestro/images/expected/ios/progress_bar.png b/maestro/images/expected/ios/progress_bar.png index 384490fd5..35935f43d 100644 Binary files a/maestro/images/expected/ios/progress_bar.png and b/maestro/images/expected/ios/progress_bar.png differ diff --git a/maestro/images/expected/ios/progress_circle.png b/maestro/images/expected/ios/progress_circle.png index c23e815e4..8910b107b 100644 Binary files a/maestro/images/expected/ios/progress_circle.png and b/maestro/images/expected/ios/progress_circle.png differ diff --git a/maestro/images/expected/ios/qr_code.png b/maestro/images/expected/ios/qr_code.png index d9e332874..b398e2e92 100644 Binary files a/maestro/images/expected/ios/qr_code.png and b/maestro/images/expected/ios/qr_code.png differ diff --git a/maestro/images/expected/ios/radio_buttons.png b/maestro/images/expected/ios/radio_buttons.png index 389324451..0a3cb1819 100644 Binary files a/maestro/images/expected/ios/radio_buttons.png and b/maestro/images/expected/ios/radio_buttons.png differ diff --git a/maestro/images/expected/ios/range_slider.png b/maestro/images/expected/ios/range_slider.png index 8fe49e000..eac756913 100644 Binary files a/maestro/images/expected/ios/range_slider.png and b/maestro/images/expected/ios/range_slider.png differ diff --git a/maestro/images/expected/ios/rating.png b/maestro/images/expected/ios/rating.png index bd79a1a93..ccf0c840b 100644 Binary files a/maestro/images/expected/ios/rating.png and b/maestro/images/expected/ios/rating.png differ diff --git a/maestro/images/expected/ios/safe_area_view_bottom_bar.png b/maestro/images/expected/ios/safe_area_view_bottom_bar.png index 3737fbcfa..8446cb2f3 100644 Binary files a/maestro/images/expected/ios/safe_area_view_bottom_bar.png and b/maestro/images/expected/ios/safe_area_view_bottom_bar.png differ diff --git a/maestro/images/expected/ios/safe_area_view_container.png b/maestro/images/expected/ios/safe_area_view_container.png index 934bcd5b4..798479616 100644 Binary files a/maestro/images/expected/ios/safe_area_view_container.png and b/maestro/images/expected/ios/safe_area_view_container.png differ diff --git a/maestro/images/expected/ios/safe_area_view_image.png b/maestro/images/expected/ios/safe_area_view_image.png index 274952ffb..d2f62d366 100644 Binary files a/maestro/images/expected/ios/safe_area_view_image.png and b/maestro/images/expected/ios/safe_area_view_image.png differ diff --git a/maestro/images/expected/ios/safe_area_view_list_view.png b/maestro/images/expected/ios/safe_area_view_list_view.png index a5f0cc3ab..5070dc3c7 100644 Binary files a/maestro/images/expected/ios/safe_area_view_list_view.png and b/maestro/images/expected/ios/safe_area_view_list_view.png differ diff --git a/maestro/images/expected/ios/safe_area_view_text.png b/maestro/images/expected/ios/safe_area_view_text.png index 4f7028d8a..dc45dfdcd 100644 Binary files a/maestro/images/expected/ios/safe_area_view_text.png and b/maestro/images/expected/ios/safe_area_view_text.png differ diff --git a/maestro/images/expected/ios/safe_area_view_top_bar.png b/maestro/images/expected/ios/safe_area_view_top_bar.png index 7435c94f0..1e73832e2 100644 Binary files a/maestro/images/expected/ios/safe_area_view_top_bar.png and b/maestro/images/expected/ios/safe_area_view_top_bar.png differ diff --git a/maestro/images/expected/ios/slider.png b/maestro/images/expected/ios/slider.png index d9fe876da..22814cd71 100644 Binary files a/maestro/images/expected/ios/slider.png and b/maestro/images/expected/ios/slider.png differ diff --git a/maestro/images/expected/ios/toggle_buttons.png b/maestro/images/expected/ios/toggle_buttons.png index e03c20f3b..a1f7ea7c1 100644 Binary files a/maestro/images/expected/ios/toggle_buttons.png and b/maestro/images/expected/ios/toggle_buttons.png differ diff --git a/maestro/images/expected/ios/toggle_sidebar.png b/maestro/images/expected/ios/toggle_sidebar.png index e324279a3..23c6f7462 100644 Binary files a/maestro/images/expected/ios/toggle_sidebar.png and b/maestro/images/expected/ios/toggle_sidebar.png differ diff --git a/maestro/run_maestro_jsactions_tests.sh b/maestro/run_maestro_jsactions_tests.sh index 204c5c59e..56bc5b595 100644 --- a/maestro/run_maestro_jsactions_tests.sh +++ b/maestro/run_maestro_jsactions_tests.sh @@ -16,7 +16,6 @@ if [ "$1" == "android" ]; then elif [ "$1" == "ios" ]; then APP_ID="com.mendix.native.template" PLATFORM="ios" - IOS_DEVICE="iPhone 16" else echo "Usage: $0 [android|ios]" exit 1 diff --git a/maestro/run_maestro_widget_tests.sh b/maestro/run_maestro_widget_tests.sh index 8298f09d0..5a7011cb6 100644 --- a/maestro/run_maestro_widget_tests.sh +++ b/maestro/run_maestro_widget_tests.sh @@ -15,7 +15,6 @@ if [ "$1" == "android" ]; then elif [ "$1" == "ios" ]; then APP_ID="com.mendix.native.template" PLATFORM="ios" - IOS_DEVICE="iPhone 16" else echo "Usage: $0 [android|ios] [widget]" exit 1 @@ -91,6 +90,12 @@ run_widget_tests() { fi } +# Fast-fail (S6): one cheap app-launch probe before any flows. A broken build fails here in +# ~1 min instead of grinding every flow × retries up to the job timeout. +if ! smoke_check; then + exit 1 +fi + # Run tests for each widget for widget in "${widgets[@]}"; do run_widget_tests "$widget" diff --git a/package.json b/package.json index dc008b4b4..7ecc069d7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "packages/**/*" ], "devDependencies": { - "@actions/core": "^1.11.1", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", "@react-native/babel-preset": "0.84.1", @@ -63,8 +62,6 @@ "image-js": "^0.37.0", "lint-staged": "^11.2.6", "mendix-client": "^7.15.8", - "pixelmatch": "^5.3.0", - "pngjs": "^6.0.0", "pretty-quick": "^3.3.1", "react-dom": "19.2.3", "recursive-copy": "^2.0.14", diff --git a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Authentication.yaml b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Authentication.yaml index ca1fa7aef..d8129f797 100644 --- a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Authentication.yaml +++ b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Authentication.yaml @@ -17,5 +17,7 @@ appId: "${APP_ID}" timeout: 5000 # 5 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/authentication" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/authentication.png" diff --git a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Clipboard.yaml b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Clipboard.yaml index eb8374bc8..ccd1aa1f3 100644 --- a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Clipboard.yaml +++ b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/Clipboard.yaml @@ -19,3 +19,5 @@ appId: "${APP_ID}" timeout: 5000 # 5 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/clipboard" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/clipboard.png" diff --git a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/DeviceInfo.yaml b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/DeviceInfo.yaml index 4a92c6324..268696659 100644 --- a/packages/jsActions/mobile-resources-native/e2e/specs/maestro/DeviceInfo.yaml +++ b/packages/jsActions/mobile-resources-native/e2e/specs/maestro/DeviceInfo.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 20000 # 20 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/device_info" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/device_info" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/device_info.png" diff --git a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/ToggleSidebar.yaml b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/ToggleSidebar.yaml index 1fc54daab..44256328c 100644 --- a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/ToggleSidebar.yaml +++ b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/ToggleSidebar.yaml @@ -19,5 +19,7 @@ appId: "${APP_ID}" timeout: 5000 # 5 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/toggle_sidebar" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/toggle_sidebar.png" diff --git a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Custom.yaml b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Custom.yaml index f007b3703..1a9d14651 100644 --- a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Custom.yaml +++ b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Custom.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" text: "Custom" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/accordion_custom" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/accordion_custom.png" diff --git a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Multiple.yaml b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Multiple.yaml index e5075ed8a..70932f520 100644 --- a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Multiple.yaml +++ b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_Multiple.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" text: "Header 1 - Text 2" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/accordion_multiple" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/accordion_multiple.png" diff --git a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_NonCollapsible.yaml b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_NonCollapsible.yaml index 87fbc848c..677d4a43b 100644 --- a/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_NonCollapsible.yaml +++ b/packages/pluggableWidgets/accordion-native/e2e/specs/maestro/Accordion_NonCollapsible.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" id: "staticImage" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/accordion_noncollapsible" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/accordion_noncollapsible.png" diff --git a/packages/pluggableWidgets/animation-native/e2e/specs/maestro/Animation.yaml b/packages/pluggableWidgets/animation-native/e2e/specs/maestro/Animation.yaml index 167e657bb..b84799f95 100644 --- a/packages/pluggableWidgets/animation-native/e2e/specs/maestro/Animation.yaml +++ b/packages/pluggableWidgets/animation-native/e2e/specs/maestro/Animation.yaml @@ -14,3 +14,5 @@ appId: "${APP_ID}" timeout: 5000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/animation" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/animation.png" diff --git a/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml b/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml index 8aa3307a7..a731fdba2 100644 --- a/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml +++ b/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml @@ -13,6 +13,8 @@ appId: "${APP_ID}" timeout: 10000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/background_gradient" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/background_gradient.png" - tapOn: id: "bgGradientTwoColors" - assertVisible: diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_clickable_container.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_clickable_container.yaml index bf47ed7ae..a3da372f7 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_clickable_container.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_clickable_container.yaml @@ -11,4 +11,6 @@ appId: "${APP_ID}" - tapOn: id: "clickableContainer" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_clickable_container" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_clickable_container" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_clickable_container.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_conditional_visibility.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_conditional_visibility.yaml index 76c573d3a..07787c002 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_conditional_visibility.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_conditional_visibility.yaml @@ -10,4 +10,6 @@ appId: "${APP_ID}" text: "Conditional visibility" - assertVisible: "Condtional visibility" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_conditional_visibility" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_conditional_visibility" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_conditional_visibility.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_image.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_image.yaml index 499009f01..3d1201d27 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_image.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_image.yaml @@ -14,4 +14,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 30000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_image" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_image" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_dynamic_image.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_svg.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_svg.yaml index b69ebc87a..270bcd7c4 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_svg.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_dynamic_svg.yaml @@ -14,4 +14,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 15000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_svg" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_svg" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_dynamic_svg.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_layout_grid.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_layout_grid.yaml index a9a87ded8..2edf99791 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_layout_grid.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_layout_grid.yaml @@ -15,4 +15,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 15000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_layout_grid" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_layout_grid" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_layout_grid.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_nested.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_nested.yaml index 5ba8267ab..2939a3cbd 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_nested.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_nested.yaml @@ -15,4 +15,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 15000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_nested" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_nested" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_nested.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_images.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_images.yaml index 441c547f7..ead2fafdb 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_images.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_images.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 10000 # 10 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/bg_image_static_images" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/bg_image_static_images" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_static_images.png" diff --git a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_svg.yaml b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_svg.yaml index 9160fedee..d87fd0cb2 100644 --- a/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_svg.yaml +++ b/packages/pluggableWidgets/background-image-native/e2e/specs/maestro/BgImage_static_svg.yaml @@ -11,4 +11,6 @@ appId: "${APP_ID}" - assertVisible: "Static SVG images" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_static_svg" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bg_image_static_svg.png" diff --git a/packages/pluggableWidgets/bar-chart-native/e2e/specs/maestro/Bar_Chart.yaml b/packages/pluggableWidgets/bar-chart-native/e2e/specs/maestro/Bar_Chart.yaml index 56939851a..3648a1efa 100644 --- a/packages/pluggableWidgets/bar-chart-native/e2e/specs/maestro/Bar_Chart.yaml +++ b/packages/pluggableWidgets/bar-chart-native/e2e/specs/maestro/Bar_Chart.yaml @@ -17,4 +17,6 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bar_chart" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bar_chart.png" diff --git a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Bottom_sheet_expanding.yaml b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Bottom_sheet_expanding.yaml index 557eedb80..288323c33 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Bottom_sheet_expanding.yaml +++ b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Bottom_sheet_expanding.yaml @@ -15,3 +15,5 @@ appId: "${APP_ID}" timeout: 10000 # 10 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_expanding" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bottom_sheet_expanding.png" diff --git a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Botton_sheet_expanding_fullscreen.yaml b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Botton_sheet_expanding_fullscreen.yaml index 1bf0b138d..c7c3a9a5c 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Botton_sheet_expanding_fullscreen.yaml +++ b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Botton_sheet_expanding_fullscreen.yaml @@ -15,3 +15,5 @@ appId: "${APP_ID}" timeout: 10000 # 10 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_expanding_fullscreen" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bottom_sheet_expanding_fullscreen.png" diff --git a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic_non_native.yaml b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic_non_native.yaml index ac67b21a7..199199179 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic_non_native.yaml +++ b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic_non_native.yaml @@ -19,3 +19,5 @@ appId: "${APP_ID}" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_modal_basic_non_native" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bottom_sheet_modal_basic_non_native.png" diff --git a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_custom.yaml b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_custom.yaml index 372a875a2..30848c8ef 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_custom.yaml +++ b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_custom.yaml @@ -17,3 +17,5 @@ appId: "${APP_ID}" timeout: 5000 # 5 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_modal_custom" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/bottom_sheet_modal_custom.png" diff --git a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Conditional.yaml b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Conditional.yaml index 75ab87235..0fb46f901 100644 --- a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Conditional.yaml +++ b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Conditional.yaml @@ -12,5 +12,7 @@ appId: "${APP_ID}" text: "Color picker - Conditional" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/color_picker_conditional" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/color_picker_conditional.png" diff --git a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Disabled.yaml b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Disabled.yaml index afda9100a..8abfa8c53 100644 --- a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Disabled.yaml +++ b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Disabled.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" text: "Color picker - Disabled" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/color_picker_disabled" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/color_picker_disabled.png" diff --git a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Normal.yaml b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Normal.yaml index c904f07f8..2fbc83fe3 100644 --- a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Normal.yaml +++ b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Normal.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" text: "Color picker - Normal" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/color_picker_normal" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/color_picker_normal.png" diff --git a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Partial.yaml b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Partial.yaml index 7dda95169..d46086afb 100644 --- a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Partial.yaml +++ b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_Partial.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" text: "Color picker - Partial" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/color_picker_partial" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/color_picker_partial.png" diff --git a/packages/pluggableWidgets/column-chart-native/e2e/specs/maestro/Column_chart_native.yaml b/packages/pluggableWidgets/column-chart-native/e2e/specs/maestro/Column_chart_native.yaml index 4edb88ddf..9152df5c3 100644 --- a/packages/pluggableWidgets/column-chart-native/e2e/specs/maestro/Column_chart_native.yaml +++ b/packages/pluggableWidgets/column-chart-native/e2e/specs/maestro/Column_chart_native.yaml @@ -17,5 +17,7 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/column_chart" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/column_chart.png" diff --git a/packages/pluggableWidgets/floating-action-button-native/e2e/specs/maestro/FloatingActionButton.yaml b/packages/pluggableWidgets/floating-action-button-native/e2e/specs/maestro/FloatingActionButton.yaml index 50ab2404e..069d02166 100644 --- a/packages/pluggableWidgets/floating-action-button-native/e2e/specs/maestro/FloatingActionButton.yaml +++ b/packages/pluggableWidgets/floating-action-button-native/e2e/specs/maestro/FloatingActionButton.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" id: "floatingActionButtonTopRight" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/floating_action_button" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/floating_action_button.png" diff --git a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml index de48606ce..7342bdeed 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml @@ -15,3 +15,5 @@ appId: "${APP_ID}" timeout: 30000 # 30 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_dynamic" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/image_dynamic.png" diff --git a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml index 26f77efc7..be3d6a184 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml @@ -12,3 +12,5 @@ appId: "${APP_ID}" text: "Image Icon" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_icon" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/image_icon.png" diff --git a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml index 1d10ca136..f4bffeb2c 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml @@ -17,3 +17,5 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_static" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/image_static.png" diff --git a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml index 704bef241..236c90555 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" timeout: 20000 # 20 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_url" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/image_url.png" diff --git a/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml b/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml index 7ddfd9aa1..454efa6f1 100644 --- a/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml +++ b/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 15000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/line_chart" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/line_chart" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/line_chart.png" diff --git a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_custom.yaml b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_custom.yaml index 00fd50798..ff5e169c9 100644 --- a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_custom.yaml +++ b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_custom.yaml @@ -16,4 +16,6 @@ appId: "${APP_ID}" optional: true # This should be true so that the test won't fail timeout: 15000 # 15 seconds - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/doughnut_chart_custom" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/doughnut_chart_custom" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/doughnut_chart_custom.png" diff --git a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_multiple_data_points.yaml b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_multiple_data_points.yaml index 9af2068f0..70e1d7236 100644 --- a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_multiple_data_points.yaml +++ b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Doughnut_chart_multiple_data_points.yaml @@ -17,4 +17,6 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/doughnut_chart_multiple_data_points" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/doughnut_chart_multiple_data_points.png" diff --git a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_custom.yaml b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_custom.yaml index ec2c608ce..6d4119d06 100644 --- a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_custom.yaml +++ b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_custom.yaml @@ -19,3 +19,5 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/pie_chart_custom" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/pie_chart_custom.png" diff --git a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_multiple_data_points.yaml b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_multiple_data_points.yaml index 042b88aaf..6be2273b0 100644 --- a/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_multiple_data_points.yaml +++ b/packages/pluggableWidgets/pie-doughnut-chart-native/e2e/specs/maestro/Pie_chart_multiple_data_points.yaml @@ -19,3 +19,5 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/pie_chart_multiple_data_points" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/pie_chart_multiple_data_points.png" diff --git a/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml b/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml index 5a173a3dd..6d722ca09 100644 --- a/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml +++ b/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml @@ -17,5 +17,7 @@ appId: "${APP_ID}" text: "Alert" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/popup_menu_alert" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/popup_menu_alert.png" diff --git a/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml b/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml index ad279383b..e308f907c 100644 --- a/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml +++ b/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml @@ -10,4 +10,6 @@ appId: "${APP_ID}" text: "Dynamic values:" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/progress_bar" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/progress_bar.png" diff --git a/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml b/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml index 4bdffc1fd..3eecce7e5 100644 --- a/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml +++ b/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" text: "With default text:" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/progress_circle" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/progress_circle.png" diff --git a/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml b/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml index 25ce7c35d..a3d4fc37e 100644 --- a/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml +++ b/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" id: "qRCodeNormal" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/qr_code" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/qr_code.png" diff --git a/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml b/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml index f8903ab7d..51d16d555 100644 --- a/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml +++ b/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" text: "Vertical radio buttons" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/radio_buttons" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/radio_buttons.png" diff --git a/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml b/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml index 5c5eb6937..3ae1024c9 100644 --- a/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml +++ b/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" text: "Dynamic ranges" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/range_slider" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/range_slider.png" diff --git a/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml b/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml index ae20c68b6..1f6e081f0 100644 --- a/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml +++ b/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml @@ -10,3 +10,5 @@ appId: "${APP_ID}" text: "Normal:" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/rating" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/rating.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewBottomBar.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewBottomBar.yaml index 6afebae92..6aa01c4c8 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewBottomBar.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewBottomBar.yaml @@ -15,3 +15,5 @@ appId: "${APP_ID}" \ - Bottom bar starts directly below safe area" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/safe_area_view_bottom_bar" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_bottom_bar.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewContainer.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewContainer.yaml index a6f43bb97..50d1503d4 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewContainer.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewContainer.yaml @@ -18,4 +18,6 @@ appId: "${APP_ID}" \ - White container uses all available space (flex 1)" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/safe_area_view_container" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/safe_area_view_container" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_container.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewImage.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewImage.yaml index 33cab68ad..8ba33f913 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewImage.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewImage.yaml @@ -11,4 +11,6 @@ appId: "${APP_ID}" - assertVisible: id: "image1" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/safe_area_view_image" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/safe_area_view_image" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_image.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewListView.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewListView.yaml index dc951ffa5..0c899f9ff 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewListView.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewListView.yaml @@ -14,4 +14,6 @@ appId: "${APP_ID}" id: "image1" index: 14 - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/safe_area_view_list_view" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/safe_area_view_list_view" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_list_view.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewText.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewText.yaml index cd4d83c1e..9370b8d6b 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewText.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewText.yaml @@ -14,4 +14,6 @@ appId: "${APP_ID}" Expected result: Text rendered in safe area and full blue background" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/safe_area_view_text" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/safe_area_view_text" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_text.png" diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewTopBar.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewTopBar.yaml index b5ce46034..595508ebc 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewTopBar.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewTopBar.yaml @@ -14,4 +14,6 @@ appId: "${APP_ID}" \ - Safe area starts directly below header" - takeScreenshot: - path: "maestro/images/actual/${PLATFORM}/safe_area_view_top_bar" \ No newline at end of file + path: "maestro/images/actual/${PLATFORM}/safe_area_view_top_bar" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/safe_area_view_top_bar.png" diff --git a/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml b/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml index 369b9e55c..f087e375d 100644 --- a/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml +++ b/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml @@ -9,3 +9,5 @@ appId: "${APP_ID}" - assertVisible: "Dynamic ranges" - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/slider" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/slider.png" diff --git a/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml b/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml index 0e92f2645..d21739775 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml +++ b/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml @@ -15,3 +15,5 @@ appId: "${APP_ID}" timeout: 15000 # 15 seconds - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/toggle_buttons" +- assertScreenshot: + path: "maestro/images/expected/${PLATFORM}/toggle_buttons.png" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9873dfbc..859a17c69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,9 +52,6 @@ importers: .: devDependencies: - '@actions/core': - specifier: ^1.11.1 - version: 1.11.1 '@commitlint/cli': specifier: ^19.8.1 version: 19.8.1(@types/node@20.19.41)(typescript@5.9.3) @@ -121,12 +118,6 @@ importers: mendix-client: specifier: ^7.15.8 version: 7.15.8 - pixelmatch: - specifier: ^5.3.0 - version: 5.3.0 - pngjs: - specifier: ^6.0.0 - version: 6.0.0 pretty-quick: specifier: ^3.3.1 version: 3.3.1(prettier@2.8.8) @@ -975,18 +966,6 @@ importers: packages: - '@actions/core@1.11.1': - resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==} - - '@actions/exec@1.1.1': - resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==} - - '@actions/http-client@2.2.3': - resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} - - '@actions/io@1.1.3': - resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} - '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -5650,10 +5629,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pixelmatch@5.3.0: - resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} - hasBin: true - pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -5673,10 +5648,6 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} - pngjs@6.0.0: - resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} - engines: {node: '>=12.13.0'} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -6967,10 +6938,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - two-product@1.0.2: resolution: {integrity: sha512-vOyrqmeYvzjToVM08iU52OFocWT6eB/I5LUWYnxeAPGXAhAxXYU/Yr/R2uY5/5n4bvJQL9AQulIuxpIsMoT8XQ==} @@ -7038,10 +7005,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@8.3.0: - resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} - engines: {node: '>=22.19.0'} - unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -7478,22 +7441,6 @@ packages: snapshots: - '@actions/core@1.11.1': - dependencies: - '@actions/exec': 1.1.1 - '@actions/http-client': 2.2.3 - - '@actions/exec@1.1.1': - dependencies: - '@actions/io': 1.1.3 - - '@actions/http-client@2.2.3': - dependencies: - tunnel: 0.0.6 - undici: 8.3.0 - - '@actions/io@1.1.3': {} - '@adobe/css-tools@4.4.4': {} '@babel/code-frame@7.29.0': @@ -13497,10 +13444,6 @@ snapshots: pirates@4.0.7: {} - pixelmatch@5.3.0: - dependencies: - pngjs: 6.0.0 - pkg-dir@3.0.0: dependencies: find-up: 3.0.0 @@ -13521,8 +13464,6 @@ snapshots: pngjs@5.0.0: {} - pngjs@6.0.0: {} - possible-typed-array-names@1.1.0: {} postcss-calc@8.2.4(postcss@8.5.15): @@ -14930,8 +14871,6 @@ snapshots: tslib@2.8.1: {} - tunnel@0.0.6: {} - two-product@1.0.2: {} two-sum@1.0.0: {} @@ -14999,8 +14938,6 @@ snapshots: undici-types@6.21.0: {} - undici@8.3.0: {} - unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: