Skip to content

test(android): fix E2E animation-timeout with offline WebView spinner app - #156

Closed
pr4bh4sh wants to merge 68 commits into
devicelab-dev:mainfrom
pr4bh4sh:fix/android-webview-animation-test
Closed

test(android): fix E2E animation-timeout with offline WebView spinner app#156
pr4bh4sh wants to merge 68 commits into
devicelab-dev:mainfrom
pr4bh4sh:fix/android-webview-animation-test

Conversation

@pr4bh4sh

Copy link
Copy Markdown

Fixes the failing E2E Android (API 36) check on PR #1. The test_wait_for_animation_never_ends tests (Python + TypeScript) asserted waitForAnimationToEnd times out on an infinitely-animating screen, but they opened the camera expecting a live preview. On the CI emulator the camera preview returns byte-identical frames, so the driver saw 0.0% diff and concluded the screen was static (success=True) instead of timing out. Server logic is correct; the test premise was wrong.

Fix: replace the camera with a tiny offline native Android app (dev.maestro.animationtest) showing a full-screen WebView rendering a perpetually rotating CSS spinner via loadDataWithBaseURL (no network, no local server). A WebView's CSS animation is independent of the system animator/window/transition scales, so it keeps rotating even with disable-animations: true — unlike a native View/Animator. Tests launch the app, poll adb dumpsys window to confirm it is foreground, then call waitForAnimationToEnd, which now reliably times out.

Changes: e2e/android-animation-app/ (app source), drivers/android/animation-test-app-debug.apk (prebuilt, per existing convention), .github/workflows/e2e-android.yml (install APK), and the two never-ends test rewrites.

Verified locally on an emulator: both device tests pass; no regressions in other device tests; ruff/mypy/tsc/eslint pass. (go test has 2 environmental failures due to a connected emulator, unrelated to this change.)

omnarayan and others added 30 commits March 9, 2026 13:18
On Android 13+, dumpsys window InputMethod contains both mFrame= (full
InputMethod window starting at status bar y=84) and touchable region=
(actual keyboard area). The code checked mFrame first, causing every
element to appear covered by the keyboard. Swap strategy order to prefer
touchable region on Android 13+, falling back to mFrame for Android <=12.
Introduce a session-based HTTP server that bridges JSON requests to
core.Driver, enabling programmatic test execution without YAML flow files.

- Add server package (pkg/server) with session management, endpoints for
  execute, screenshot, source hierarchy, device info, and graceful shutdown
- Add `server` CLI subcommand with configurable port and signal handling
- Add JSON unmarshaling for flow steps (pkg/flow/json.go) mirroring the
  existing YAML parser
- Add json struct tags to all step types and Selector for serialization
- Add server and JSON unmarshaling tests
- Update documentation to reflect new server capabilities and usage instructions
…sions

The previous fix required isOnScreen=true to use touchable region, but
SDK 30 may lack the isOnScreen field entirely, falling back to mFrame
which returns the full InputMethod window bounds. Now check negative
signals (isOnScreen=false, mViewVisibility=0x8) to reject hidden
keyboards, then always prefer touchable region over mFrame.
Verified against AOSP source (Android 10, 11, 13) that isOnScreen=,
mViewVisibility=, touchable region=, and mGivenContentInsets= are
present on all versions. The parsing now uses a 3-strategy approach:

1. touchable region (most accurate, used by stock keyboards)
2. mFrame + mGivenContentInsets (for vendor keyboards like Samsung/Xiaomi
   that don't set touchable insets — content insets reveal keyboard top)
3. mFrame alone with 60% height sanity check (rejects full-screen
   InputMethod windows that would cause false positives)

Negative signals (isOnScreen=false, mViewVisibility=0x8) bail early.
- Add t.Parallel() to slow/timeout-dependent tests across all drivers
  (uiautomator2, wda, appium, devicelab, flutter) to speed up test runs
- Extract EmulatorStarter interface in cli/test.go to decouple
  handleEmulatorStartup from concrete emulator.Manager, enabling
  proper unit testing with a mockEmulatorStarter
- Fix findScrollableElement in uiautomator2 to return early when page
  source has elements but none are scrollable, avoiding unnecessary waits
- Update airplane mode tests to match new "cmd connectivity airplane-mode"
  implementation (Android 11+) instead of legacy "settings put" commands
- Provide mock sourceData for swipe tests that rely on page source parsing
- Set explicit short FindTimeout on tests that previously used defaults,
  preventing slow failures in relative selector and element-not-found tests
…tion fix

- Add 'sleep' command support (YAML and JSON) with FlowRunner execution
  for both top-level and nested steps
- Rework hideKeyboard to try multiple strategies (Appium → ESCAPE → BACK)
  with optional 'approach' field to force a specific method
- Add isInputShown() using 'dumpsys input_method' for reliable keyboard
  detection on SDK 36+ where 'dumpsys window InputMethod' no longer
  reports mInputShown
- Fix hideKeyboard JSON unmarshaling (was missing json.Unmarshal call)
- Register StepSleep in YAML parser with scalar (ms) and mapping syntax
…hangelog

- Add isKeyboardVisible step type for querying soft keyboard state,
  returning boolean in CommandResult.Data
- Rename hideKeyboard 'approach' field to 'strategy' for consistency;
  accept 'esc' as alias for 'escape'
- Expose KeyboardVisible in StateSnapshot via GetState()
- Add IsKeyboardVisible() public method on uiautomator2 Driver
- Update CHANGELOG with sleep, isKeyboardVisible, and hideKeyboard
  strategy entries
Add client/python package with:
- MaestroClient with session management and context manager support
- Command methods: tap, input_text, swipe, scroll, assert_visible,
  launch_app, screenshot, device_info, etc.
- Typed models (DeviceInfo, CommandResult, StepResult) with dataclasses
- Custom exceptions (SessionError, StepError, ConnectionError)
- Unit tests with mock HTTP server (test_client.py, test_models.py)
- E2E test examples (test_e2e_android.py, test_add_contact.py)
- Page object pattern example (tests/pages/)
- .gitignore for venv, __pycache__, dist, .pytest_cache
Add client/typescript package with:
- MaestroClient class with session management and auto-cleanup
- Command methods: tap, inputText, swipe, scroll, assertVisible,
  launchApp, screenshot, deviceInfo, hideKeyboard, sleep, etc.
- Typed models (DeviceInfo, CommandResult, StepResult) with interfaces
- Custom exceptions (SessionError, StepError, ConnectionError)
- E2E test example with Jest (test_add_contact.test.ts)
- Page object pattern example (tests/pages/)
- Jest config, tsconfig, and .gitignore for node_modules/dist
…DEVELOPER.md

- Add eslint.config.mjs (ESLint v9 flat config) with typescript-eslint
  rules: consistent-type-imports, no-explicit-any (warn in src, off in
  tests), no-console (warn in src), strict equality, curly braces
- Fix type-only imports in client.ts and commands.ts per lint rules
- Fix curly brace style in models.ts (containsChild assignment)
- Add lint and lint:fix npm scripts
- Add DEVELOPER.md with project structure, build/test/lint instructions,
  code conventions, and guide for adding new commands
…ce test

- Rework conftest.py to support pytest-xdist: each worker gets its own
  maestro-runner server on a unique port targeting a specific device via
  adb device discovery and ANDROID_SERIAL assignment
- Add _discover_devices() and _worker_index() helpers for parallel setup
- maestro_server fixture now yields (url, device_serial) tuple
- client fixture passes deviceId capability when running in parallel
- Add test_contact_persists.py: POM-based test that creates a contact,
  cold-relaunches the app, and verifies the contact persists
- Update README with parallel test instructions and env var reference
…ut protection

- Add Info logging across all 7 CDP connection steps (detect, source, connection,
  forward, websocket, browser, ready) plus disconnect and cleanup
- Add document.visibilityState check before CDP element finding — skip CDP entirely
  when WebView is not visible (background tab), eliminating wasted round-trips
- Add 3s timeout on all Rod/CDP calls to prevent indefinite hangs when WebView is
  suspended or unresponsive
- Add automatic cleanup (Rod browser, ADB forward, local socket) when dead connection
  is detected during element finding
- Collapse AX tree text search from 12 CDP round-trips to 1 call with Go-side role
  prioritization
…ct persistence tests

TypeScript:
- Rework tests/setup.ts to support Jest parallel workers: each worker
  gets its own maestro-runner server on a unique port with a specific
  device assigned via adb discovery and JEST_WORKER_ID
- Add jest-html-reporters and jest-junit for HTML and JUnit XML reports
- Configure reporters in jest.config.js (output to ./reports/)
- Add test_contact_persists.test.ts: POM-based test that creates a
  contact, cold-relaunches the app, and verifies persistence

Python:
- Add pytest-html to dev dependencies for HTML report generation
- Configure pytest addopts for HTML and JUnit XML report output
- Add setuptools package discovery config for maestro_runner
Previously, on every launchApp the Flutter connection state was reset
unconditionally, causing repeated discovery attempts for apps that
were already determined to not be Flutter. Now only resets if a
connection was previously established. Also moves attempted=true
to the top of reconnect methods so failures don't retry every step.
Rename jsHelperCode to JSHelperCode so the devicelab driver can
inject the same element-finding helpers into WebView pages.
Add ForwardToAbstractSocket, ForwardTCPToAbstractSocket,
RemoveTCPForward, and CDPSocketPath to support forwarding
CDP traffic from local sockets to on-device WebView debug sockets.
Introduce core.Element interface with Text, Input, and Clear methods.
NativeElement wraps UIAutomator2 elements, WebElement wraps Rod/CDP
elements — allowing DeviceLab commands to interact with both uniformly.
… CDP

Replace direct ActiveElement/SendKeys calls with findFocused() returning
a core.Element, enabling unified text input for native and web elements.
Add ensureWebViewConnection() to tapOn for automatic WebView detection.
Wire SetWebViewForwarder and Close in driver setup.
Bundle the on-device instrumentation agent (Android) and
WebDriverAgent replacement (iOS) source projects for building
the DeviceLab native drivers.
…ELOPER.md

- pyproject.toml: add ruff >=0.4.0, mypy >=1.10, types-requests dev deps;
  configure ruff (line-length=100, E/W/F/I/B/UP/N/S/RUF rule sets, S10x
  ignored in tests) and mypy (strict mode on maestro_runner/)
- Makefile: add lint-py (ruff check + mypy) and lint-py-fix targets
- DEVELOPER.md: new file documenting setup, lint, test, and code conventions
- __init__.py: sort __all__ entries alphabetically; reorder imports
- client.py: wrap long lines (>100 chars) to satisfy E501
- commands.py: explicit str() cast for selector text; wrap long dict literal
- conftest.py: use collections.abc.Generator (UP035); fix blank-line isort
- contact_list_page.py / edit_contact_page.py: fix import ordering (I001)
- test_add_contact.py / test_contact_persists.py: fix blank-line import order
- test_client.py: remove trailing blank line before import block
- test_e2e_android.py: add timeout=30 to requests.post (S113); fix imports
- test_models.py: remove unused pytest import (F401); remove blank line
- Add pushy descriptions to combat undertriggering (Make sure to use...)
- Add explicit negative triggers (DO NOT use for) in description
- Add allowed-tools and metadata fields (author, version, category, tags)
- Move all when-to-use info to description field (not body)
- Replace CRITICAL headers with explain-the-why rationale
- Add step-numbered workflow sections for explicit ordering
- Add Do NOT use section in body for quick disambiguation
UIA2 Keyboard Tests:
- Add mInputShown=true gating to keyboard visibility shell mock responses
  in keyboard_test.go, driver_test.go, and commands_test.go
- Keyboard bounds/visibility now properly verified only when IME input
  is active on device

CLI Parallel Mode Tests:
- Introduce autoDetectDevicesFn function variable for test injection
- Override in TestDetermineExecutionMode_ParallelWithoutAutoStart to avoid
  environment-sensitive adb device detection
- Ensures CI/test determinism independent of emulator/device state

Keyboard Hide Command Tests:
- Enhance MockShellExecutor with sequential responses (pop-from-slice)
- TestHideKeyboardSuccess now provides pre/post visibility states
- Handles pre-check (mInputShown=true) and post-check (mInputShown=false)

Python Skill Documentation:
- Add e2e Android test lock troubleshooting (socket-based process lock)
- Document venv path requirements (client/python/.venv)
- Add Reliable Full-Client Run Order section (unit → server → e2e)
- Capture lessons: avoid combined pytest runs on single emulator

New Skill: merge-upstream
- Create .github/skills/merge-upstream/SKILL.md
- Hardcode upstream URL: https://github.com/devicelab-dev/maestro-runner
- Full merge workflow: fetch, merge, conflict resolution, verification
- Includes troubleshooting for common merge scenarios
prabhash-nw and others added 29 commits March 13, 2026 15:34
Add new git-commit skill with conventional commits workflow and evals.
Improve descriptions for all skills to trigger more reliably:
- go-test-runner: expand triggers, add find/head/tail/tee to allowed-tools
- merge-upstream: expand triggers, improve stash guidance, consolidate test steps
- python-test-runner: add Step 4 iOS test section, improve run-all sequence
- typescript-test-runner: expand triggers and instructions

Add evals/evals.json for every skill to enable performance benchmarking.
# Conflicts:
#	pkg/driver/browser/cdp/jshelper.go
…roid

ci(workflows): add unit test jobs and Android e2e workflow
Integrating latest changes from https://github.com/devicelab-dev/maestro-runner:
- Add cloud provider abstraction for test result reporting (SauceLabs support)
- Fix CDP waitForPageReady crash
- Update DeviceLab Android driver APK
- Clean up dead code and update airplane mode tests
- Update changelog and release notes for v1.1.1

Resolved merge conflicts in:
- .gitignore (kept both entries)
- pkg/flutter/semantics.go (removed unused regex patterns)
- pkg/driver/uiautomator2/commands_test.go (kept local comments)
Move skill configurations and GitHub templates from .github/ to repository root:
- Moved ISSUE_TEMPLATE, PULL_REQUEST_TEMPLATE.md, copilot-instructions.md to root
- Moved workflows directory to root
- Consolidated skills directory in .claude/skills/ (proper location)
- Removed now-empty .github directory
- Restore fork's multi-strategy hideKeyboard (appium/escape/back) and its
  tests; upstream's keyboard_hide_test.go was added by the merge and targets
  upstream's implementation, so it is removed to keep the fork's tests green.
- Make getKeyboardBounds parse 'dumpsys window InputMethod' directly and have
  parseKeyboardFrame treat mInputShown=false as not-shown, so the devicelab-dev#139
  keyboard-blocking regression tests pass.
- Keep fork's Condition.Script under the upstream 'true' yaml key (matches the
  fork's parser_test.go).
Unify the four screenshot-based drivers (uiautomator2, wda,
devicelab, appium) behind a single core.WaitForScreenStatic
implementation. All now honor step.sleepMs/threshold and return
Success:false with a "Timed out" message when the screen never
settles, restoring the fork's fail-on-timeout semantics and
dropping appium's duplicate PNG decoder in favor of
core.ImageDifference.
The shared conftest opens one session per test process via the autouse
_track_client -> client fixture. test_e2e_android.py opened a second
session, which the uiautomator2 device lock (/tmp/uia2-<serial>.sock)
rejected with "device already in use". Reuse the conftest client session
via the session_id fixture (yielding client.session_id) so only one
session is active; teardown stays with the client fixture.
In the TypeScript test setup, teardown killed the maestro-runner server
but left its stdout/stderr piped to serverLogStream. After the stream was
ended, the exiting child flushed final output into it, throwing
"write after end" and crashing the runner after tests had already passed.
Unpipe the child's streams before ending the log stream.
…esting guidance

Un-ignore .claude/skills so skill definitions are versioned. Extract the
common device-harness error-handling (uiautomator2 single-session lock,
stale /tmp/uia2-* PID guard, pkill self-match, teardown stream race) into a
shared device-integration-testing skill that the Python and TypeScript
test-runner skills now reference instead of duplicating.

Co-Authored-By: opencode <noreply@opencode.ai>
GitHub Actions only reads workflows from .github/workflows/, so the
root workflows/ files were never picked up (the historical CI runs
predate today's main overwrite). Move ci.yml and e2e-android.yml into
the standard location so they run.

Co-Authored-By: opencode <noreply@opencode.ai>
Exercise waitForAnimationToEnd in CI: the Python settle test
(test_wait_for_animation_to_end.py) and the TypeScript timeout test
(test_wait_for_animation_never_ends.device.test.ts), which guard the
fail-on-timeout behaviour introduced by the animation fix.

Co-Authored-By: opencode <noreply@opencode.ai>
…imeout

The waitForAnimationToEnd timeout test relied on an external spinner page
(discuss.wxpython.org) that renders static (or is unreachable) inside the CI
sandbox, so the screen was already static and the test false-failed. Serve a
local CSS spinner from an HTTP server on the runner, reached by the emulator
at 10.0.2.2, so the infinite animation is reliably reproduced. Also fix the
TS teardown: server.close() blocks on the browser's keep-alive connection, so
force-close connections first (same class of bug as setup.ts). Add the Python
timeout test to the E2E workflow so both clients guard the fix.

Co-Authored-By: opencode <noreply@opencode.ai>
The locally-served spinner (and the earlier external URL) both failed in CI
because the GitHub-hosted emulator cannot reach the runner's loopback and
Chrome won't render a local file:// from an intent. Replace them with the
emulator's camera preview, a live feed that never settles and needs no
network or browser. This makes the waitForAnimationToEnd timeout guard
reliable in CI. Drop the now-unneeded HTTP server (and its closeAllConnections
teardown fix) from the TS test.

Co-Authored-By: opencode <noreply@opencode.ai>
# Conflicts:
#	.gitignore
#	pkg/flow/step.go
Add a "Language Clients" section to the README and dedicated tutorials under
docs/clients/ covering setup, selectors, the Page Object Model pattern, advanced
calls, and parallel execution. These clients (and the REST API server they wrap)
are local additions not yet present in upstream.
* fix(lint): clear golangci-lint and ruff issues blocking CI

These were pre-existing repo-wide lint failures (not introduced by the client
expansion) that blocked the PR's Test Go and Test Python Client jobs:

- errcheck: ignore close/remove error returns in webview.go, devicelab_ios/
  setup.go, wda/runner.go, flutter/wrapper.go, simulator/recording.go
- staticcheck (ST1005): drop trailing punctuation from a build.go error string
- unused: remove dead waitKeyboardHidden in uiautomator2/keyboard.go
- ruff (S607): noqa on subprocess.run(["adb", ...]) in the animation test

The TypeScript client job and Python mypy/pytest already passed.

* chore(ci): trigger workflow run for lint PR
…browser steps (#1)

* feat(clients): expand typed bindings with gesture, media, device and browser steps

Add typed MaestroClient methods for the most common step types not yet wrapped in
the TypeScript and Python clients:

- Gestures: doubleTapOn, longPressOn, dragAndDrop, scrollUntilVisible
- Assertions & media: assertScreenshot, takeScreenshot, copyTextFrom, pasteText, setClipboard
- AI & scripting: assertWithAI, evalScript, runScript, evalBrowserScript
- Device control: setLocation, setAirplaneMode, toggleAirplaneMode,
  setNetworkConditions, openNotifications, setDarkMode, setOrientation
- Browser (web): openBrowser, switchTab, closeTab, getConsoleLogs,
  clearConsoleLogs, assertNoJSErrors, mockNetwork

Each method maps 1:1 to a server step type. The low-level executeStep/execute_step
still forwards any raw step dict, so the remaining ~90 server step types remain
reachable. Docs (client READMEs + tutorials) updated with a full method table and
examples.

* fix(lint): clear golangci-lint and ruff issues blocking CI
@pr4bh4sh pr4bh4sh closed this Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants