diff --git a/.github/actions/archive-test-results/action.yaml b/.github/actions/archive-test-results/action.yaml index 4c4146091..3a6a164d1 100644 --- a/.github/actions/archive-test-results/action.yaml +++ b/.github/actions/archive-test-results/action.yaml @@ -12,12 +12,16 @@ inputs: required: false default: ${{ github.workspace }} +# Every step below is `if: ${{ !cancelled() }}` rather than `if: always()`. Both still run +# when the test step failed — which is the case we need these artifacts for — but always() +# is exempt from cancellation, so cancelling a run left each shard uploading five artifact +# sets before it could die, which is why "Cancel workflow" appeared to do nothing. runs: using: composite steps: - name: Archive runtime logs uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 - if: always() + if: ${{ !cancelled() }} with: name: ${{ inputs.platform }}-runtime-logs-${{ inputs.test-type }} path: log/*.log @@ -25,7 +29,7 @@ runs: - name: Archive test screenshots uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 - if: always() + if: ${{ !cancelled() }} with: name: ${{ inputs.platform }}-screenshots-${{ inputs.test-type }} path: ${{ inputs.workspace-path }}/maestro/images/actual/${{ inputs.platform }}/**/*.png @@ -38,7 +42,7 @@ runs: # 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() + if: ${{ !cancelled() }} shell: bash run: | dest="${{ inputs.workspace-path }}/maestro/images/diff/${{ inputs.platform }}" @@ -49,7 +53,7 @@ runs: - name: Archive screenshot diffs uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 - if: always() + if: ${{ !cancelled() }} with: name: ${{ inputs.platform }}-diffs-${{ inputs.test-type }} path: ${{ inputs.workspace-path }}/maestro/images/diff/${{ inputs.platform }}/*.png @@ -57,7 +61,7 @@ runs: - name: Archive artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7 - if: always() + if: ${{ !cancelled() }} with: name: ${{ inputs.platform }}-artifacts-${{ inputs.test-type }} path: packages/pluggableWidgets/**/artifacts/ @@ -68,7 +72,7 @@ runs: # 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() + if: ${{ !cancelled() }} with: name: ${{ inputs.platform }}-videos-${{ inputs.test-type }} path: maestro/videos/*.mp4 diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index a51dee981..e5d4b67c2 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -9,6 +9,15 @@ on: branches: - main - 'mx/**' + +# Supersede in-flight runs: a new push to the same PR/branch cancels the previous run +# instead of queueing beside it (this repo shares GitHub Free's 20-concurrent-job cap with +# NativePipeline, so stale runs directly delay the ones that matter). Only PR runs are +# cancelled — a push run on main/mx-* is the recorded result for that commit, so let it finish. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: test: name: "Build (${{ matrix.os }})" diff --git a/.github/workflows/NativePipeline.yml b/.github/workflows/NativePipeline.yml index ca85b0e0f..bce173335 100644 --- a/.github/workflows/NativePipeline.yml +++ b/.github/workflows/NativePipeline.yml @@ -105,7 +105,7 @@ env: # 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 || 'a915484ef2cfed403cfbdfa531638df3c46c9d00' }} + NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || '05255f5ad19de7b109f6a68088e4fb198447ed00' }} permissions: packages: write jobs: @@ -418,7 +418,7 @@ jobs: 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') }} + if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && !cancelled() && (needs.project.result == 'success') }} steps: - name: Debug branch value run: echo "Using branch ${{ needs.determine-nt-version.outputs.nt_branch }}" @@ -535,7 +535,7 @@ jobs: 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') }} + if: ${{ github.event.inputs.LOCAL_TEST_ARTIFACT_RUN_ID == '' && !cancelled() && (needs.project.result == 'success') }} steps: - name: "Check out Native Template for Native Components Test Project" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -647,7 +647,7 @@ jobs: android-widget-tests: 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') }} + if: ${{ needs.scope.outputs.widgets_to_test != '[]' && !cancelled() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} 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 @@ -731,10 +731,12 @@ jobs: 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 + # !cancelled(): 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() }} + # for the failing ones we actually need to debug. This covers pass AND fail, but + # unlike always() it is NOT exempt from cancellation, so a cancelled run stops here + # instead of first uploading five artifact sets per shard. + if: ${{ !cancelled() }} uses: ./.github/actions/archive-test-results with: platform: android @@ -743,7 +745,7 @@ jobs: ios-widget-tests: 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') }} + if: ${{ needs.scope.outputs.widgets_to_test != '[]' && !cancelled() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} 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 @@ -751,7 +753,7 @@ jobs: # 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: 35 + timeout-minutes: 30 strategy: max-parallel: 4 matrix: @@ -803,8 +805,10 @@ jobs: 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() }} + # !cancelled(): capture failure artifacts (the failing shard's test step exits + # non-zero), but do NOT hold a cancelled run open uploading artifacts nobody wants + # -- always() is exempt from cancellation, !cancelled() is not. + if: ${{ !cancelled() }} uses: ./.github/actions/archive-test-results with: platform: ios @@ -813,7 +817,7 @@ jobs: android-js-tests: 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') }} + if: ${{ needs.scope.outputs.js_actions_changed == 'true' && !cancelled() && needs.project.result == 'success' && (needs.android-app.result == 'success' || needs.android-app.result == 'skipped') }} runs-on: ubuntu-26.04 timeout-minutes: 90 steps: @@ -883,8 +887,10 @@ jobs: 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() }} + # !cancelled(): capture failure artifacts (the failing shard's test step exits + # non-zero), but do NOT hold a cancelled run open uploading artifacts nobody wants + # -- always() is exempt from cancellation, !cancelled() is not. + if: ${{ !cancelled() }} uses: ./.github/actions/archive-test-results with: platform: android @@ -893,7 +899,7 @@ jobs: ios-js-tests: 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') }} + if: ${{ needs.scope.outputs.js_actions_changed == 'true' && !cancelled() && needs.project.result == 'success' && (needs.ios-app.result == 'success' || needs.ios-app.result == 'skipped') }} runs-on: macos-26 timeout-minutes: 90 steps: @@ -941,8 +947,10 @@ jobs: 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() }} + # !cancelled(): capture failure artifacts (the failing shard's test step exits + # non-zero), but do NOT hold a cancelled run open uploading artifacts nobody wants + # -- always() is exempt from cancellation, !cancelled() is not. + if: ${{ !cancelled() }} uses: ./.github/actions/archive-test-results with: platform: ios @@ -954,9 +962,11 @@ jobs: # 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() }} + # Run even when shards fail or time out — those are exactly the ones whose + # screenshots/logs we want to inspect. !cancelled() rather than always() so an + # explicitly cancelled run isn't held open merging artifacts nobody asked for + # (a shard that hits timeout-minutes does not make cancelled() true). + if: ${{ !cancelled() }} 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 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 61116a795..6a471037d 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -6,6 +6,14 @@ on: pull_request: branches: [main, 'mx/11.12.x'] +# Supersede in-flight runs: a new push to the same PR/branch cancels the previous run +# instead of queueing beside it (this repo shares GitHub Free's 20-concurrent-job cap with +# NativePipeline, so stale runs directly delay the ones that matter). Only PR runs are +# cancelled — a push run on main/mx-* is the recorded result for that commit, so let it finish. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: test: name: "Release (${{ matrix.os }})" diff --git a/.github/workflows/ShaCheck.yml b/.github/workflows/ShaCheck.yml index 70fff8b42..d701e4482 100644 --- a/.github/workflows/ShaCheck.yml +++ b/.github/workflows/ShaCheck.yml @@ -10,6 +10,14 @@ on: paths: - ".github/workflows/*.yml" +# Supersede in-flight runs: a new push to the same PR/branch cancels the previous run +# instead of queueing beside it (this repo shares GitHub Free's 20-concurrent-job cap with +# NativePipeline, so stale runs directly delay the ones that matter). Only PR runs are +# cancelled — a push run on main/mx-* is the recorded result for that commit, so let it finish. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: harden_security: name: Check SHA in GH Actions diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml index ecd5b36e6..2313ffe84 100644 --- a/.github/workflows/UnitTests.yml +++ b/.github/workflows/UnitTests.yml @@ -6,6 +6,14 @@ on: pull_request: branches: [main, 'mx/11.12.x'] +# Supersede in-flight runs: a new push to the same PR/branch cancels the previous run +# instead of queueing beside it (this repo shares GitHub Free's 20-concurrent-job cap with +# NativePipeline, so stale runs directly delay the ones that matter). Only PR runs are +# cancelled — a push run on main/mx-* is the recorded result for that commit, so let it finish. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: test: name: "Unit tests" diff --git a/maestro/helpers/helpers.sh b/maestro/helpers/helpers.sh index d035668e1..4da373502 100644 --- a/maestro/helpers/helpers.sh +++ b/maestro/helpers/helpers.sh @@ -78,13 +78,62 @@ stop_recording() { REC_FILE="" } +# Function to clean up stale XCTest processes (iOS only) +cleanup_xctest_processes() { + if [ "$PLATFORM" != "ios" ]; then + return 0 + fi + echo "🧹 Cleaning up stale XCTest processes..." + # Kill any leftover XCTest runner processes that might be blocking driver initialization + pkill -9 -f "XCTRunner" 2>/dev/null || true + pkill -9 -f "xctest" 2>/dev/null || true + pkill -9 -f "maestro.*driver" 2>/dev/null || true + sleep 2 +} + # Function to restart the iOS simulator +# +# This is the recovery path taken when the Maestro/XCTest driver wedges ("iOS driver not +# ready in time"), so it has to ACTUALLY restart the device. It previously did not: the +# shutdown result was discarded with `|| true`, and the following prepare_ios.sh reported +# +# Monitoring boot status for iPhone 17 Pro (...). +# Device already booted, nothing to do. +# +# i.e. the device never went down and the shard only got an app reinstall on top of the +# same wedged runtime. So we now verify the device reaches Shutdown and say so loudly if +# it doesn't, rather than silently continuing with a half-reset simulator. restart_simulator() { echo "🔄 Restarting iOS Simulator..." - # 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 + # Kill the XCTest runner FIRST: while it holds the device, shutdown can fail. + cleanup_xctest_processes + + # Target the device prepare_ios.sh selected when we know it; `all` otherwise (first + # call in a shard, where SIMULATOR_DEVICE_ID isn't exported into this shell yet). + local target="${SIMULATOR_DEVICE_ID:-all}" + echo "Shutting down simulator ($target)..." + if ! xcrun simctl shutdown "$target" 2>&1; then + # Already-shutdown is a benign error, so don't fail here — the poll below is what + # decides whether the device is actually down. + echo "shutdown reported an error; verifying device state anyway" + fi + + # `simctl shutdown ` is synchronous (measured ~3s), but poll rather than trust + # it: this replaces a blind `sleep 10` that padded every retry whether or not it was + # needed. Bounded so a stuck device surfaces as a warning instead of hanging. + local deadline=$((SECONDS + 30)) + while [ "$SECONDS" -lt "$deadline" ]; do + if [ -n "${SIMULATOR_DEVICE_ID:-}" ]; then + xcrun simctl list devices | grep -q "$SIMULATOR_DEVICE_ID.*Shutdown" && break + else + xcrun simctl list devices | grep -q "(Booted)" || break + fi + sleep 1 + done + if [ "$SECONDS" -ge "$deadline" ]; then + echo "::warning::Simulator did not reach Shutdown within 30s; the driver may still be wedged after this restart" + fi + bash ./maestro/helpers/prepare_ios.sh } diff --git a/maestro/helpers/prepare_ios.sh b/maestro/helpers/prepare_ios.sh index c914b9a4f..9acd85a2a 100644 --- a/maestro/helpers/prepare_ios.sh +++ b/maestro/helpers/prepare_ios.sh @@ -64,6 +64,34 @@ elif best: xcrun simctl bootstatus "$DEVICE_ID" -b || echo "Simulator booted" } +# Cut UIKit animation time, mirroring `disable-animations: true` on the Android emulator +# (NativePipeline.yml) — iOS previously had no equivalent, so every navigation push/modal +# played at full length on a 3-vCPU runner while Android's ran instantly. +# +# Scope: this shortens SYSTEM transitions (nav pushes, modal presents, alerts). React +# Native's `Animated` does NOT read this flag automatically, so widget-internal animations +# (carousel, animation-native) are unaffected — this is not a substitute for the per-flow +# waits those need. +# +# Requires a BOOTED device (`simctl spawn`), so call after start_simulator. The value is +# written to the device's persistent domain and survives shutdown/boot, so a +# restart_simulator in helpers.sh keeps it. +reduce_motion() { + echo "Reducing motion on iOS Simulator..." + if [ -z "$SIMULATOR_DEVICE_ID" ]; then + echo "Error: SIMULATOR_DEVICE_ID not set" + return 1 + fi + # Best-effort: a future runtime that drops/renames these keys should not fail the shard, + # since tests still pass with animations on (just slower). + xcrun simctl spawn "$SIMULATOR_DEVICE_ID" defaults write com.apple.Accessibility ReduceMotionEnabled -bool true \ + || echo "::warning::Could not set ReduceMotionEnabled; animations stay at full length" + # Replaces the sliding push/pop transition with a cross-fade — the slide is what Maestro + # most often races against when asserting right after a navigation tap. + xcrun simctl spawn "$SIMULATOR_DEVICE_ID" defaults write com.apple.Accessibility ReduceMotionReduceSlideTransitionsEnabled -bool true \ + || echo "::warning::Could not set ReduceMotionReduceSlideTransitionsEnabled" +} + set_status_bar() { echo "Setting status bar on iOS Simulator..." if [ -z "$SIMULATOR_DEVICE_ID" ]; then @@ -92,6 +120,7 @@ verify_installed_app() { } start_simulator +reduce_motion set_status_bar install_ios_app verify_installed_app \ No newline at end of file diff --git a/maestro/images/expected/android/bg_image_dynamic_svg.png b/maestro/images/expected/android/bg_image_dynamic_svg.png index ebd38ac4c..77122942e 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/ios/bg_image_dynamic_svg.png b/maestro/images/expected/ios/bg_image_dynamic_svg.png new file mode 100644 index 000000000..91393cdfb Binary files /dev/null and b/maestro/images/expected/ios/bg_image_dynamic_svg.png differ diff --git a/maestro/maestro/images/actual/ios/authentication.png b/maestro/maestro/images/actual/ios/authentication.png deleted file mode 100644 index 235b9e00a..000000000 Binary files a/maestro/maestro/images/actual/ios/authentication.png and /dev/null differ diff --git a/maestro/maestro/images/actual/ios/bar_chart.png b/maestro/maestro/images/actual/ios/bar_chart.png deleted file mode 100644 index a4cc7b9e7..000000000 Binary files a/maestro/maestro/images/actual/ios/bar_chart.png and /dev/null differ diff --git a/maestro/maestro/images/actual/ios/clipboard.png b/maestro/maestro/images/actual/ios/clipboard.png deleted file mode 100644 index ed3ed3d93..000000000 Binary files a/maestro/maestro/images/actual/ios/clipboard.png and /dev/null differ diff --git a/maestro/maestro/images/actual/ios/device_info.png b/maestro/maestro/images/actual/ios/device_info.png deleted file mode 100644 index e906d88a6..000000000 Binary files a/maestro/maestro/images/actual/ios/device_info.png and /dev/null differ diff --git a/maestro/maestro/images/actual/ios/safe_area_view_text.png b/maestro/maestro/images/actual/ios/safe_area_view_text.png deleted file mode 100644 index 4f9622d5d..000000000 Binary files a/maestro/maestro/images/actual/ios/safe_area_view_text.png and /dev/null differ diff --git a/maestro/maestro/images/actual/ios/toggle_sidebar.png b/maestro/maestro/images/actual/ios/toggle_sidebar.png deleted file mode 100644 index 362cc34eb..000000000 Binary files a/maestro/maestro/images/actual/ios/toggle_sidebar.png and /dev/null differ diff --git a/maestro/run_maestro_jsactions_tests.sh b/maestro/run_maestro_jsactions_tests.sh index 56bc5b595..f4f7c5a7b 100644 --- a/maestro/run_maestro_jsactions_tests.sh +++ b/maestro/run_maestro_jsactions_tests.sh @@ -97,6 +97,16 @@ run_jsactions_tests() { fi } +# Clean up any stale XCTest processes before starting (iOS only) +if [ "$PLATFORM" == "ios" ]; then + cleanup_xctest_processes +fi + +# Fast-fail smoke check before running jsActions tests (same as widget tests) +if ! smoke_check; then + exit 1 +fi + # Run jsActions tests run_jsactions_tests if [ $? -ne 0 ]; then 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 a731fdba2..f0469e710 100644 --- a/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml +++ b/packages/pluggableWidgets/background-gradient-native/e2e/specs/maestro/BackgroundGradient.yaml @@ -5,6 +5,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background gradient" + timeout: 10000 - tapOn: text: "Background gradient" - extendedWaitUntil: 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 a3da372f7..08ae75d23 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: 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 07787c002..a635d4307 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: 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 3d1201d27..677282fd7 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 @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: text: "Dynamic image" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 30000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 30s. +- waitForAnimationToEnd: + timeout: 30000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_image" - assertScreenshot: 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 270bcd7c4..9bdf144f0 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 @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: text: "Dynamic SVG image" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_dynamic_svg" - assertScreenshot: 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 2edf99791..c5ae84e99 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 @@ -4,16 +4,21 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: text: "Layout grid" - assertVisible: "Layout grid" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_layout_grid" - assertScreenshot: 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 2939a3cbd..e75896ac1 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 @@ -4,16 +4,21 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: text: "Nested" - assertVisible: "Nested" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_nested" - assertScreenshot: 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 ead2fafdb..470510cea 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: text: "Static images" - assertVisible: text: "Static images" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 10000 # 10 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 10s. +- waitForAnimationToEnd: + timeout: 10000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bg_image_static_images" - assertScreenshot: 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 d87fd0cb2..da053eeca 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Background image" + timeout: 10000 - tapOn: text: "Background image" - tapOn: diff --git a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_android.yaml b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_android.yaml index 9593da840..600fd9bc6 100644 --- a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_android.yaml +++ b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_android.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Badge" + timeout: 10000 - tapOn: text: "Badge" - tapOn: diff --git a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml index 395c52e37..af56d79f0 100644 --- a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml +++ b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Badge" + timeout: 10000 - tapOn: text: "Badge" - inputText: 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 3648a1efa..b533564e0 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bar chart" + timeout: 10000 - tapOn: text: "Bar chart" - assertVisible: text: "Bar chart" - assertVisible: id: "container1" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bar_chart" - assertScreenshot: diff --git a/packages/pluggableWidgets/barcode-scanner-native/e2e/specs/maestro/Barcode_scanner.yaml b/packages/pluggableWidgets/barcode-scanner-native/e2e/specs/maestro/Barcode_scanner.yaml index d210dbc54..a394cbd20 100644 --- a/packages/pluggableWidgets/barcode-scanner-native/e2e/specs/maestro/Barcode_scanner.yaml +++ b/packages/pluggableWidgets/barcode-scanner-native/e2e/specs/maestro/Barcode_scanner.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Barcode scanner" + timeout: 10000 - tapOn: text: "Barcode scanner" - assertVisible: 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 288323c33..ea32686b5 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 @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bottom Sheet" + timeout: 10000 - tapOn: text: "Bottom Sheet" - tapOn: text: "Expanding" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 10000 # 10 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 10s. +- waitForAnimationToEnd: + timeout: 10000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_expanding" - assertScreenshot: 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 c7c3a9a5c..08df165a1 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 @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bottom Sheet" + timeout: 10000 - tapOn: text: "Bottom Sheet" - tapOn: text: "Expanding fullscreen" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 10000 # 10 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_expanding_fullscreen" - assertScreenshot: diff --git a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic.yaml b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic.yaml index 9f969d39c..7784f2d7f 100644 --- a/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic.yaml +++ b/packages/pluggableWidgets/bottom-sheet-native/e2e/specs/maestro/Modal_basic.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bottom Sheet" + timeout: 10000 - tapOn: text: "Bottom Sheet" - tapOn: 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 199199179..08faf3d04 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 @@ -4,17 +4,26 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bottom Sheet" + timeout: 10000 - tapOn: text: "Bottom Sheet" - tapOn: text: "Modal basic non native" +# Wait for the page to fully load before tapping Open +- extendedWaitUntil: + visible: "Open" + timeout: 10000 - tapOn: text: "Open" -# Wait until the sheet is actually open (button label changes) -- extendedWaitUntil: - visible: randText - optional: true +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 25s. +- waitForAnimationToEnd: timeout: 25000 - takeScreenshot: 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 30848c8ef..c18d50855 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "B" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Bottom Sheet" + timeout: 10000 - tapOn: text: "Bottom Sheet" - tapOn: text: "Modal custom" - tapOn: text: "Open" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 5000 # 5 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 5s. +- waitForAnimationToEnd: + timeout: 5000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/bottom_sheet_modal_custom" - assertScreenshot: diff --git a/packages/pluggableWidgets/carousel-native/e2e/specs/maestro/Carousel.yaml b/packages/pluggableWidgets/carousel-native/e2e/specs/maestro/Carousel.yaml index 0daafad85..b0b2e2ac0 100644 --- a/packages/pluggableWidgets/carousel-native/e2e/specs/maestro/Carousel.yaml +++ b/packages/pluggableWidgets/carousel-native/e2e/specs/maestro/Carousel.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Carousel" + timeout: 10000 - tapOn: text: "Carousel" - swipe: 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 0fb46f901..cc5d3aaa8 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Color picker" + timeout: 10000 - tapOn: text: "Color picker" - tapOn: 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 8abfa8c53..269eeb85a 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Color picker" + timeout: 10000 - tapOn: text: "Color picker" - tapOn: 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 2fbc83fe3..442e014b4 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Color picker" + timeout: 10000 - tapOn: text: "Color picker" - tapOn: diff --git a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_OnChange.yaml b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_OnChange.yaml index 284e3188d..a6b1cf898 100644 --- a/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_OnChange.yaml +++ b/packages/pluggableWidgets/color-picker-native/e2e/specs/maestro/Color_picker_native_OnChange.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Color picker" + timeout: 10000 - tapOn: text: "Color picker" - tapOn: 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 d46086afb..0b0b310d4 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Color picker" + timeout: 10000 - tapOn: text: "Color picker" - tapOn: 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 9152df5c3..41fd2699a 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "C" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Column chart" + timeout: 10000 - tapOn: text: "Column chart" - assertVisible: text: "Column chart" - assertVisible: id: "container1" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/column_chart" - assertScreenshot: diff --git a/packages/pluggableWidgets/feedback-native/e2e/specs/maestro/Feedback.yaml b/packages/pluggableWidgets/feedback-native/e2e/specs/maestro/Feedback.yaml index 008a3b27e..c18b6b911 100644 --- a/packages/pluggableWidgets/feedback-native/e2e/specs/maestro/Feedback.yaml +++ b/packages/pluggableWidgets/feedback-native/e2e/specs/maestro/Feedback.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "F" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Feedback" + timeout: 10000 - tapOn: text: "Feedback" - assertVisible: 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 069d02166..8a815d513 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "F" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Floating action button" + timeout: 10000 - tapOn: text: "Floating action button" - tapOn: diff --git a/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native.yaml b/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native.yaml index c1dcba938..745551557 100644 --- a/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native.yaml +++ b/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "G" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Gallery" + timeout: 10000 - tapOn: text: "Gallery" - tapOn: diff --git a/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native_horizontal.yaml b/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native_horizontal.yaml index 13a977858..36291bd50 100644 --- a/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native_horizontal.yaml +++ b/packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native_horizontal.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "G" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Gallery" + timeout: 10000 - tapOn: text: "Gallery" - tapOn: diff --git a/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml b/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml index c6b7d63a5..be0d04d17 100644 --- a/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml +++ b/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "G" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Gallery" + timeout: 10000 - tapOn: text: "Gallery" - tapOn: 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 7342bdeed..b32c3880e 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_dynamic.yaml @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "I" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Image" + timeout: 10000 - tapOn: text: "Image" - tapOn: text: "Image dynamic" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 30000 # 30 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 30s. +- waitForAnimationToEnd: + timeout: 30000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_dynamic" - assertScreenshot: 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 be3d6a184..4b7fd64c4 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_icon.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "I" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Image" + timeout: 10000 - tapOn: text: "Image" - tapOn: 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 f4bffeb2c..61c59f5bf 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_static.yaml @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "I" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Image" + timeout: 10000 - tapOn: text: "Image" - tapOn: text: "Image static" - assertVisible: text: "Main as Static Image; Default as Static; Background as Contain" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_static" - assertScreenshot: 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 236c90555..e1ab212ea 100644 --- a/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml +++ b/packages/pluggableWidgets/image-native/e2e/specs/maestro/Image_url.yaml @@ -4,16 +4,21 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "I" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Image" + timeout: 10000 - tapOn: text: "Image" - tapOn: text: "Image url" - assertVisible: "Main as Image URL(svg); Background as No; Action as Open dialog box" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 20000 # 20 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 20s. +- waitForAnimationToEnd: + timeout: 20000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/image_url" - assertScreenshot: diff --git a/packages/pluggableWidgets/intro-screen-native/CHANGELOG.md b/packages/pluggableWidgets/intro-screen-native/CHANGELOG.md index b44994119..5f4666072 100644 --- a/packages/pluggableWidgets/intro-screen-native/CHANGELOG.md +++ b/packages/pluggableWidgets/intro-screen-native/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue where reopening a page containing the intro screen would show the first slide instead of the slide referenced by the active slide attribute, and would report a slide change that the user did not make. +- We fixed an issue where the initial slide was scrolled to with an animation, which briefly left the slide content unavailable to screen readers. +- We fixed an issue where moving to a slide with the next, previous or pagination controls could jump back to the first slide and report an extra slide change. + ## [4.4.1] - 2026-6-10 ### Changed diff --git a/packages/pluggableWidgets/intro-screen-native/e2e/specs/maestro/IntroScreen.yaml b/packages/pluggableWidgets/intro-screen-native/e2e/specs/maestro/IntroScreen.yaml index 9df303212..069bcc029 100644 --- a/packages/pluggableWidgets/intro-screen-native/e2e/specs/maestro/IntroScreen.yaml +++ b/packages/pluggableWidgets/intro-screen-native/e2e/specs/maestro/IntroScreen.yaml @@ -4,11 +4,19 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "I" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Intro screen" + timeout: 10000 - tapOn: text: "Intro screen" +# Opening the page mounts the widget and waits on the runtime, unlike the in-flow waits +# below which only cover an on-device slide transition. 5s was flaky on the CI simulator +# (3 vCPU, shared with the Mendix runtime JVM); 15s matches the repo's usual budget. - extendedWaitUntil: visible: "Active slide: 2" - timeout: 5000 + timeout: 15000 - assertVisible: text: "Changes: 0" - swipe: @@ -42,11 +50,16 @@ appId: "${APP_ID}" - tapOn: "Go back" - tapOn: text: "Intro screen" +# Re-entry remounts the widget, which must reposition to the attribute's slide — same +# page-entry cost as the first open, so the same 15s budget rather than 5s. - extendedWaitUntil: visible: "Active slide: 2" - timeout: 5000 + timeout: 15000 - tapOn: text: "NEXT" +- extendedWaitUntil: + visible: "Active slide: 3" + timeout: 5000 - tapOn: text: "FINISH" - extendedWaitUntil: diff --git a/packages/pluggableWidgets/intro-screen-native/src/SwipeableContainer.tsx b/packages/pluggableWidgets/intro-screen-native/src/SwipeableContainer.tsx index c64934610..ca0f90d32 100644 --- a/packages/pluggableWidgets/intro-screen-native/src/SwipeableContainer.tsx +++ b/packages/pluggableWidgets/intro-screen-native/src/SwipeableContainer.tsx @@ -69,9 +69,17 @@ const refreshActiveSlideAttribute = (slides: SlidesType[], activeSlide?: Editabl export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement => { const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); - const [activeIndex, setActiveIndex] = useState(0); + // Start on the slide the attribute points at, so activeIndex never lags the slide that + // initialScrollIndex renders. Otherwise the first Next/Previous press moves relative to + // slide 0 instead of the visible slide. + const [activeIndex, setActiveIndex] = useState(() => refreshActiveSlideAttribute(props.slides, props.activeSlide)); const flashList = useRef>(null); - const isInitializing = useRef(true); + const hasAppliedInitialScroll = useRef(false); + const isUserDragging = useRef(false); + // Slide a programmatic scroll is currently heading for, or null when the list is + // wherever the user left it. Used to recognise momentum events that contradict the + // scroll we just asked for; see onMomentumScrollEnd. + const pendingScrollTarget = useRef(null); const rtlSafeIndex = useCallback( (i: number): number => (isAndroidRTL ? props.slides.length - 1 - i : i), @@ -79,11 +87,13 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement ); const goToSlide = useCallback( - (pageNum: number) => { + (pageNum: number, animated = true) => { setActiveIndex(pageNum); if (flashList && flashList.current) { + pendingScrollTarget.current = pageNum; flashList.current.scrollToOffset({ - offset: rtlSafeIndex(pageNum) * width + offset: rtlSafeIndex(pageNum) * width, + animated }); } }, @@ -91,19 +101,21 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement ); useEffect(() => { + if (!width || props.activeSlide?.status !== ValueStatus.Available) { + return; + } const slide = refreshActiveSlideAttribute(props.slides, props.activeSlide); - if (width && props.activeSlide?.status === ValueStatus.Available && slide !== activeIndex) { + // Once width is known, force the list to the attribute's slide even when activeIndex + // already matches: initialScrollIndex is applied before layout, so on remount the list + // can sit at offset 0 while activeIndex says otherwise. + if (!hasAppliedInitialScroll.current) { + hasAppliedInitialScroll.current = true; + // Jump without animation: activeIndex is applied immediately, and it drives which + // slide is exposed to accessibility. An animated scroll would leave the exposed + // slide off-screen until the animation lands. + goToSlide(slide, false); + } else if (slide !== activeIndex) { goToSlide(slide); - if (isInitializing.current) { - if (isInitializing.current) { - // Use requestAnimationFrame twice to wait for the next frame after scroll. - requestAnimationFrame(() => { - requestAnimationFrame(() => { - isInitializing.current = false; - }); - }); - } - } } }, [props.activeSlide, activeIndex, width, props.slides, goToSlide]); @@ -315,15 +327,46 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement ); }; + const onScrollBeginDrag = useCallback(() => { + isUserDragging.current = true; + // The user takes over from here, so any programmatic scroll still in flight no + // longer describes where the list is heading. Clearing the target also keeps it + // from latching when a scroll never reports a momentum end of its own. + pendingScrollTarget.current = null; + }, []); + const onMomentumScrollEnd = useCallback( (event: NativeSyntheticEvent) => { + const wasUserDragging = isUserDragging.current; + isUserDragging.current = false; + + if (!width) { + return; + } const offset = event.nativeEvent.contentOffset.x; const newIndex = rtlSafeIndex(Math.round(offset / width)); + + // While a programmatic scroll is in flight the list can report a momentum end + // for the position it is leaving rather than the one it is heading to. Trusting + // that offset rewrites activeIndex to a slide that is not on screen, and the + // drag flag cannot tell the two apart because a fling reports momentum without + // a fresh drag. Anything that disagrees with the requested slide is stale. + const pendingTarget = pendingScrollTarget.current; + if (pendingTarget !== null) { + if (newIndex === pendingTarget) { + pendingScrollTarget.current = null; + } + return; + } + if (newIndex === activeIndex) { return; } - if (isInitializing.current) { + // Only a user swipe reports a change here. Programmatic scrolls (initial positioning, + // Next/Previous/pagination) already fired onSlideChange, so re-firing would double + // count the change and, on remount, emit a spurious change back to slide 1. + if (!wasUserDragging) { setActiveIndex(newIndex); return; } @@ -366,6 +409,7 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement bounces={false} style={styles.flatList} renderItem={renderItem} + onScrollBeginDrag={onScrollBeginDrag} onMomentumScrollEnd={onMomentumScrollEnd} scrollEventThrottle={50} extraData={[width, activeIndex]} diff --git a/packages/pluggableWidgets/intro-screen-native/src/__tests__/SwipeableContainer.remount.spec.tsx b/packages/pluggableWidgets/intro-screen-native/src/__tests__/SwipeableContainer.remount.spec.tsx new file mode 100644 index 000000000..e796a66b1 --- /dev/null +++ b/packages/pluggableWidgets/intro-screen-native/src/__tests__/SwipeableContainer.remount.spec.tsx @@ -0,0 +1,221 @@ +import { render, fireEvent, act } from "@testing-library/react-native"; +import { View } from "react-native"; +import { EditableValueBuilder } from "@mendix/piw-utils-internal"; +import { EditableValue } from "mendix"; +import { Big } from "big.js"; +import { SwipeableContainer } from "../SwipeableContainer"; +import { defaultWelcomeScreenStyle } from "../ui/Styles"; + +jest.mock("react-native-device-info", () => ({ + hasNotch: jest.fn(), + getDeviceId: jest.fn().mockReturnValue("") +})); + +const WIDTH = 400; + +// Capture the scroll calls the widget issues, while leaving the real FlashList in place so +// layout and momentum-scroll events still behave normally. +const scrollCalls: Array<{ offset: number; animated?: boolean }> = []; + +jest.mock("@shopify/flash-list", () => { + const actual = jest.requireActual("@shopify/flash-list"); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const react = require("react"); + const Wrapped = react.forwardRef((props: any, ref: any) => { + const inner = react.useRef(null); + react.useImperativeHandle(ref, () => ({ + scrollToOffset: (params: any) => { + (globalThis as any).__introScreenScrollCalls.push(params); + (inner.current as any)?.scrollToOffset?.(params); + } + })); + return react.createElement(actual.FlashList, { ...props, ref: inner }); + }); + return { ...actual, FlashList: Wrapped }; +}); + +(globalThis as any).__introScreenScrollCalls = scrollCalls; + +type Setup = ReturnType & { + activeSlide: EditableValue; + onSlideChange: jest.Mock; + scrollCalls: Array<{ offset: number; animated?: boolean }>; +}; + +function setup(activeSlideValue: number): Setup { + const activeSlide = new EditableValueBuilder().withValue(new Big(activeSlideValue)).build(); + const onSlideChange = jest.fn(); + const slides = [1, 2, 3].map(i => ({ name: `Page ${i}`, content: })); + + scrollCalls.length = 0; + + const utils = render( + + ); + + // Simulate layout so `width` becomes known, exactly as on a real device. + act(() => { + fireEvent(utils.getByTestId("intro"), "layout", { + nativeEvent: { layout: { width: WIDTH, height: 800 } } + }); + }); + + return { ...utils, activeSlide, onSlideChange, scrollCalls }; +} + +describe("SwipeableContainer remount behaviour", () => { + it("positions the initial slide without animation", () => { + const { scrollCalls } = setup(2); + + expect(scrollCalls.length).toBeGreaterThan(0); + expect(scrollCalls[0]).toEqual({ offset: WIDTH, animated: false }); + }); + + it("writes the next slide when Next is pressed after remounting on slide 2", () => { + const { getByTestId, activeSlide } = setup(2); + + fireEvent.press(getByTestId("intro$buttonNext")); + + expect(activeSlide.setValue).toHaveBeenCalledWith(new Big(3)); + }); + + it("does not report a slide change for the programmatic initial scroll", () => { + const { getByTestId, onSlideChange, activeSlide } = setup(2); + + // The forced initial scroll settles and reports its resting offset. + act(() => { + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: WIDTH } } + }); + }); + + expect(onSlideChange).not.toHaveBeenCalled(); + expect(activeSlide.setValue).not.toHaveBeenCalled(); + }); + + it("still writes the next slide when a stale scroll settles before Next is pressed", () => { + const { getByTestId, activeSlide } = setup(2); + + act(() => { + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: WIDTH } } + }); + }); + + fireEvent.press(getByTestId("intro$buttonNext")); + + expect(activeSlide.setValue).toHaveBeenCalledWith(new Big(3)); + }); + + it("keeps the slide Next moved to when a stale momentum event reports the old offset", () => { + // Reproduces the Android e2e failure: after remounting on slide 2 the widget scrolls + // to slide 3, then the list reports a momentum end for the offset it was leaving. + const { getByTestId, activeSlide, onSlideChange } = setup(2); + + // A drag that settles without a momentum event of its own — the initial positioning + // scroll produces one on device — leaves the widget believing a drag is in progress. + act(() => { + fireEvent(getByTestId("intro"), "scrollBeginDrag", {}); + }); + + fireEvent.press(getByTestId("intro$buttonNext")); + expect(activeSlide.setValue).toHaveBeenLastCalledWith(new Big(3)); + + act(() => { + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: 0 } } + }); + }); + + // The stale offset must not drag the attribute back to slide 1 or report a change. + expect(activeSlide.setValue).toHaveBeenLastCalledWith(new Big(3)); + expect(onSlideChange).toHaveBeenCalledTimes(1); + expect(onSlideChange).toHaveBeenCalledWith(2, 1); + + // Slide 3 is the last slide, so the pagination still offers Done rather than + // falling back to the first-slide buttons. + expect(getByTestId("intro$buttonDone")).toBeTruthy(); + + // Previous moves relative to the slide that is actually on screen. + fireEvent.press(getByTestId("intro$buttonPrevious")); + expect(activeSlide.setValue).toHaveBeenLastCalledWith(new Big(2)); + }); + + it("still reports a user swipe after a programmatic scroll has settled", () => { + const { getByTestId, activeSlide, onSlideChange } = setup(2); + + fireEvent.press(getByTestId("intro$buttonNext")); + onSlideChange.mockClear(); + + // The programmatic scroll to slide 3 lands. + act(() => { + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: 2 * WIDTH } } + }); + }); + + // The user then swipes back to slide 2, which must be reported. + act(() => { + fireEvent(getByTestId("intro"), "scrollBeginDrag", {}); + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: WIDTH } } + }); + }); + + expect(onSlideChange).toHaveBeenCalledWith(1, 2); + expect(activeSlide.setValue).toHaveBeenLastCalledWith(new Big(2)); + }); + + it("reports a user swipe that interrupts an in-flight programmatic scroll", () => { + const { getByTestId, activeSlide, onSlideChange } = setup(2); + + fireEvent.press(getByTestId("intro$buttonNext")); + onSlideChange.mockClear(); + + // The user grabs the list before the scroll to slide 3 settles and swipes back to + // slide 1. Taking over invalidates the pending target, so this must be reported. + act(() => { + fireEvent(getByTestId("intro"), "scrollBeginDrag", {}); + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: 0 } } + }); + }); + + expect(onSlideChange).toHaveBeenCalledWith(0, 2); + expect(activeSlide.setValue).toHaveBeenLastCalledWith(new Big(1)); + }); + + it("ignores a stale programmatic scroll that reports the pre-scroll offset", () => { + const { getByTestId, activeSlide, onSlideChange } = setup(2); + + // The list reports offset 0 — the position it held before the programmatic scroll to + // slide 2 settled. Trusting it would desync activeIndex from what is on screen. + act(() => { + fireEvent(getByTestId("intro"), "momentumScrollEnd", { + nativeEvent: { contentOffset: { x: 0 } } + }); + }); + + expect(onSlideChange).not.toHaveBeenCalled(); + + fireEvent.press(getByTestId("intro$buttonNext")); + + expect(activeSlide.setValue).toHaveBeenCalledWith(new Big(3)); + }); +}); diff --git a/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.notch.spec.tsx.snap b/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.notch.spec.tsx.snap index 1e0342d71..b7c015e5e 100644 --- a/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.notch.spec.tsx.snap +++ b/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.notch.spec.tsx.snap @@ -55,6 +55,7 @@ exports[`Intro Screen renders 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -281,6 +282,7 @@ exports[`Intro Screen renders with 1 bottom button 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -521,6 +523,7 @@ exports[`Intro Screen renders with 2 bottom button 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -761,6 +764,7 @@ exports[`Intro Screen renders with active slide attribute 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -987,6 +991,7 @@ exports[`Intro Screen renders with async storage identifier 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} diff --git a/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.spec.tsx.snap b/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.spec.tsx.snap index e923f4fff..58f04048a 100644 --- a/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.spec.tsx.snap +++ b/packages/pluggableWidgets/intro-screen-native/src/__tests__/__snapshots__/IntroScreen.spec.tsx.snap @@ -55,6 +55,7 @@ exports[`Intro Screen renders 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -281,6 +282,7 @@ exports[`Intro Screen renders with 1 bottom button 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -521,6 +523,7 @@ exports[`Intro Screen renders with 2 bottom button 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -761,6 +764,7 @@ exports[`Intro Screen renders with active slide attribute 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} @@ -987,6 +991,7 @@ exports[`Intro Screen renders with async storage identifier 1`] = ` onLayout={[Function]} onMomentumScrollEnd={[Function]} onScroll={[Function]} + onScrollBeginDrag={[Function]} pagingEnabled={true} scrollEventThrottle={50} showsHorizontalScrollIndicator={false} 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 454efa6f1..3f18007ed 100644 --- a/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml +++ b/packages/pluggableWidgets/line-chart-native/e2e/specs/maestro/LineChart.yaml @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "L" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Line chart" + timeout: 10000 - tapOn: text: "Line chart" - assertVisible: text: "Line chart" - assertVisible: id: "container1" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/line_chart" - assertScreenshot: diff --git a/packages/pluggableWidgets/listview-swipe-native/e2e/specs/maestro/ListViewSwipe.yaml b/packages/pluggableWidgets/listview-swipe-native/e2e/specs/maestro/ListViewSwipe.yaml index 593e16115..08ada999b 100644 --- a/packages/pluggableWidgets/listview-swipe-native/e2e/specs/maestro/ListViewSwipe.yaml +++ b/packages/pluggableWidgets/listview-swipe-native/e2e/specs/maestro/ListViewSwipe.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "L" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "List view swipe" + timeout: 10000 - tapOn: text: "List view swipe" - assertVisible: diff --git a/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_dynamic.yaml b/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_dynamic.yaml index c6bca3bb5..5e7dd75e7 100644 --- a/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_dynamic.yaml +++ b/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_dynamic.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "M" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Maps" + timeout: 10000 - tapOn: text: "Maps" - tapOn: diff --git a/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_static.yaml b/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_static.yaml index 4b8280aed..6ccd10c87 100644 --- a/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_static.yaml +++ b/packages/pluggableWidgets/maps-native/e2e/specs/maestro/Maps_static.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "M" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Maps" + timeout: 10000 - tapOn: text: "Maps" - tapOn: 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 ff5e169c9..c2e9e186f 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Pie doughnut chart" + timeout: 10000 - tapOn: text: "Pie doughnut chart" - tapOn: text: "Doughnut chart Custom style" - assertVisible: id: "container1" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/doughnut_chart_custom" - assertScreenshot: 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 70e1d7236..48feeb5c3 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 @@ -4,17 +4,22 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Pie doughnut chart" + timeout: 10000 - tapOn: text: "Pie doughnut chart" - tapOn: text: "Doughnut chart Multiple data points" - assertVisible: id: "container1" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/doughnut_chart_multiple_data_points" - assertScreenshot: 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 6d4119d06..e37f6b8e6 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Pie doughnut chart" + timeout: 10000 - tapOn: text: "Pie doughnut chart" - tapOn: @@ -12,11 +17,11 @@ appId: "${APP_ID}" id: "container1" - assertVisible: id: "innerView" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/pie_chart_custom" - assertScreenshot: 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 6be2273b0..9a27b7834 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Pie doughnut chart" + timeout: 10000 - tapOn: text: "Pie doughnut chart" - tapOn: @@ -12,11 +17,11 @@ appId: "${APP_ID}" id: "container1" - assertVisible: id: "innerView" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/pie_chart_multiple_data_points" - assertScreenshot: 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 6d722ca09..f5ff3b757 100644 --- a/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml +++ b/packages/pluggableWidgets/popup-menu-native/e2e/specs/maestro/PopupMenu.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Popup menu" + timeout: 10000 - tapOn: text: "Popup menu" - tapOn: 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 e308f907c..571559349 100644 --- a/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml +++ b/packages/pluggableWidgets/progress-bar-native/e2e/specs/maestro/ProgressBar.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Progress bar" + timeout: 10000 - tapOn: text: "Progress bar" - assertVisible: 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 3eecce7e5..7788ee782 100644 --- a/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml +++ b/packages/pluggableWidgets/progress-circle-native/e2e/specs/maestro/ProgressCircle.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "P" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Progress circle" + timeout: 10000 - tapOn: text: "Progress circle" - assertVisible: 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 a3d4fc37e..aba316342 100644 --- a/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml +++ b/packages/pluggableWidgets/qr-code-native/e2e/specs/maestro/QRCode.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "Q" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "QR code" + timeout: 10000 - tapOn: text: "QR code" - assertVisible: 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 51d16d555..3b267d79b 100644 --- a/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml +++ b/packages/pluggableWidgets/radio-buttons-native/e2e/specs/maestro/RadioButtons.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "R" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Radio buttons" + timeout: 10000 - tapOn: text: "Radio buttons" - assertVisible: 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 3ae1024c9..33e3919cd 100644 --- a/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml +++ b/packages/pluggableWidgets/range-slider-native/e2e/specs/maestro/RangeSlider.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "R" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Range slider" + timeout: 10000 - tapOn: text: "Range slider" - assertVisible: diff --git a/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml b/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml index 1f6e081f0..6983d4034 100644 --- a/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml +++ b/packages/pluggableWidgets/rating-native/e2e/specs/maestro/Rating.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "R" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Rating" + timeout: 10000 - tapOn: text: "Rating" - assertVisible: diff --git a/packages/pluggableWidgets/repeater-native/e2e/specs/maestro/Repeater.yaml b/packages/pluggableWidgets/repeater-native/e2e/specs/maestro/Repeater.yaml index f9baabaac..76c220932 100644 --- a/packages/pluggableWidgets/repeater-native/e2e/specs/maestro/Repeater.yaml +++ b/packages/pluggableWidgets/repeater-native/e2e/specs/maestro/Repeater.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "R" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Repeater" + timeout: 10000 - tapOn: text: "Repeater" - assertVisible: 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 6aa01c4c8..9e94474eb 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: 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 50d1503d4..7822599dd 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: 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 8ba33f913..df3c54e0c 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: 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 0c899f9ff..1be7b0d11 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: diff --git a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewMaps.yaml b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewMaps.yaml index 9d5697c8e..0c2392d02 100644 --- a/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewMaps.yaml +++ b/packages/pluggableWidgets/safe-area-view-native/e2e/specs/maestro/SafeAreaViewMaps.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: 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 9370b8d6b..9e69233b6 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: 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 595508ebc..6cf910623 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 @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Safe area view" + timeout: 10000 - tapOn: text: "Safe area view" - tapOn: diff --git a/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml b/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml index 56b4e2da8..15f082ad9 100644 --- a/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml +++ b/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Signature" + timeout: 10000 - tapOn: text: "Signature" - swipe: diff --git a/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml b/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml index f087e375d..7cf5e3981 100644 --- a/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml +++ b/packages/pluggableWidgets/slider-native/e2e/specs/maestro/Slider.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Slider" + timeout: 10000 - tapOn: text: "Slider" - assertVisible: "Dynamic ranges" diff --git a/packages/pluggableWidgets/switch-native/e2e/specs/maestro/Switch.yaml b/packages/pluggableWidgets/switch-native/e2e/specs/maestro/Switch.yaml index dc4771da3..dfe8a60f6 100644 --- a/packages/pluggableWidgets/switch-native/e2e/specs/maestro/Switch.yaml +++ b/packages/pluggableWidgets/switch-native/e2e/specs/maestro/Switch.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "S" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Switch" + timeout: 10000 - tapOn: text: "Switch" - assertNotVisible: 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 d21739775..31b2251c7 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml +++ b/packages/pluggableWidgets/toggle-buttons-native/e2e/specs/maestro/ToggleButtons.yaml @@ -4,15 +4,20 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "T" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Toggle buttons" + timeout: 10000 - tapOn: text: "Toggle buttons" - assertVisible: text: "Default:" -# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible -- extendedWaitUntil: - visible: randText # Any random text that does not exist in the UI - optional: true # This should be true so that the test won't fail - timeout: 15000 # 15 seconds +# Wait for the screen to stop changing (image decode / sheet animation) +# rather than sleeping a fixed time: this returns as soon as the UI +# settles, and still caps at 15s. +- waitForAnimationToEnd: + timeout: 15000 - takeScreenshot: path: "maestro/images/actual/${PLATFORM}/toggle_buttons" - assertScreenshot: diff --git a/packages/pluggableWidgets/video-player-native/e2e/specs/maestro/VideoPlayer.yaml b/packages/pluggableWidgets/video-player-native/e2e/specs/maestro/VideoPlayer.yaml index b818ab081..288ce3171 100644 --- a/packages/pluggableWidgets/video-player-native/e2e/specs/maestro/VideoPlayer.yaml +++ b/packages/pluggableWidgets/video-player-native/e2e/specs/maestro/VideoPlayer.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "V" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Video player" + timeout: 10000 - tapOn: text: "Video player" - assertVisible: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Custom.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Custom.yaml index 256861033..1fd25eb33 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Custom.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Custom.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_android.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_android.yaml index ad4e571ce..0367e1660 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_android.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_android.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_ios.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_ios.yaml index 0b87db13d..b2d342382 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_ios.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_Events_ios.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_External.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_External.yaml index 9ff03a7fc..f0231107a 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_External.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_External.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_HTML.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_HTML.yaml index 5d0c777d8..c240f58ec 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_HTML.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_HTML.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_NoContent.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_NoContent.yaml index 86bdfe121..b76ef100c 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_NoContent.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_NoContent.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_URL.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_URL.yaml index 7ee208bb3..4a4ad2810 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_URL.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_URL.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: diff --git a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_UserAgent.yaml b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_UserAgent.yaml index f01fbb061..7e6a0b641 100644 --- a/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_UserAgent.yaml +++ b/packages/pluggableWidgets/web-view-native/e2e/specs/maestro/WebView_UserAgent.yaml @@ -4,6 +4,11 @@ appId: "${APP_ID}" file: "../../../../../../maestro/Precondition.yaml" - tapOn: text: "W" +# Tapping the alphabet index re-renders the widget list; wait for the +# entry before tapping it, otherwise the tap races that render. +- extendedWaitUntil: + visible: "Web view" + timeout: 10000 - tapOn: text: "Web view" - tapOn: