Skip to content

test: add Android E2E tests for editor interactions#340

Merged
dcalhoun merged 24 commits intotrunkfrom
test/android-e2e-tests
Feb 27, 2026
Merged

test: add Android E2E tests for editor interactions#340
dcalhoun merged 24 commits intotrunkfrom
test/android-e2e-tests

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Feb 26, 2026

What?

Adds an Android E2E test suite using Compose Testing + Espresso Web, mirroring the existing iOS E2E tests (EditorInteractionUITest).

Why?

iOS already has E2E tests that verify native-to-web bridge interactions — things Playwright can't test because they involve the real native configuration pipeline and toolbar. Android had no equivalent coverage. This closes that gap.

How?

  • EditorTestHelpers.kt — Reusable helper object (mirrors iOS EditorUITestHelpers.swift) with methods for navigation, text input, block insertion, mode switching, content reading, and waiting.
  • EditorInteractionTest.kt — Two test cases matching iOS:
    1. testEditorWebViewBecomesVisible — Navigates to the editor and verifies the WebView loads.
    2. testUndoRedoAfterTyping — Full bridge round-trip: type in title/content, verify undo/redo state and content assertions via code editor mode.
  • DisableAnimationsRule.kt — JUnit rule that disables device animations before each test and restores originals afterward via UiAutomation shell commands.
  • Dependencies — Espresso Web and Compose UI Test added to libs.versions.toml and app/build.gradle.kts.
  • Makefile targetstest-android-e2e and test-android-e2e-dev with auto-boot emulator logic (ENSURE_ANDROID_DEVICE macro).

Note

CI integration is not included in this PR. The Buildkite android queue agents don't currently have emulators or connected devices available for instrumented tests. CI support will be added in a follow-up once a device strategy (e.g., Gradle Managed Devices) is decided.

Testing Instructions

  1. Start an Android emulator (or let the Makefile auto-boot one).
  2. Run make test-android-e2e — both tests should pass (100%, ~13s).
  3. Alternatively, run make dev-server then make test-android-e2e-dev for dev mode.

Accessibility Testing Instructions

N/A — these are automated tests, not UI changes.

Add espresso-web to the version catalog and wire up the test
dependencies needed by the instrumented E2E suite: espresso-web for
WebView DOM interaction, compose-ui-test-junit4 for Compose semantics
assertions, and ui-test-manifest for the debug variant. Disable
animations in testOptions to avoid flakiness.
@dcalhoun dcalhoun added the [Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests. label Feb 26, 2026
Add EditorTestHelpers (mirrors iOS EditorUITestHelpers) and
EditorInteractionTest (mirrors iOS EditorInteractionUITest) with two
test cases:

- testEditorWebViewBecomesVisible: navigates Main → SitePreparation →
  Editor and verifies the Gutenberg WebView loads.
- testUndoRedoAfterTyping: full native-to-web bridge round-trip —
  types in title, inserts a Paragraph block via the web inserter,
  types in content, then verifies Undo/Redo toolbar button state
  propagates correctly through the bridge.

Key implementation details:
- Uses document.execCommand('insertText') instead of Espresso Web's
  webKeys(), which fails on Gutenberg's contenteditable rich text.
- Clicks WebView toolbar buttons via JS pointer/mouse event dispatch
  for React synthetic event compatibility.
- Inserts blocks through the web block inserter (Add block → Paragraph)
  since Android has no native block inserter.

Removes the auto-generated ExampleInstrumentedTest placeholder.
Add test-android-e2e (production build) and test-android-e2e-dev
(Vite dev server) targets, mirroring the existing test-ios-e2e /
test-ios-e2e-dev pattern.

Includes an ENSURE_ANDROID_DEVICE macro that checks for any
connected device via adb and auto-boots the first available AVD
if none is found.
@dcalhoun dcalhoun force-pushed the test/android-e2e-tests branch from 6bffae8 to 567c7c7 Compare February 26, 2026 18:20
The Android assets directory is checked into git, so the conditional
copy (only when the directory doesn't exist) would always skip,
leaving stale build output. Always clean and replace with the
current dist/ contents, matching the fix applied to iOS in PR #338.
Replace `testOptions { animationsDisabled = true }` with a custom
DisableAnimationsRule that saves the original animation scale values,
disables them for the test, and restores them in a finally block.

The Gradle setting permanently disabled animations on the emulator
with no cleanup. The rule-based approach ensures animations are
always restored, even if tests fail.
…E tests

Use semantic selectors (aria-label, aria-modal, role, placeholder) instead
of CSS class names so tests are resilient to styling refactors.

- Scope ADD_BLOCK_SELECTOR to [aria-label='Editor toolbar'] to
  disambiguate the toolbar inserter from the inline block appender
- Replace .block-editor-inserter__popover with [role='dialog'][aria-modal]
  and match block options by textContent within [role='option'] elements
- Replace textarea.editor-post-text-editor with placeholder attribute
… tests

Kotlin's assert() compiles to Java assert statements, which can be
silently disabled at the JVM level. On Android, assertions are disabled
by default unless explicitly enabled with -ea. Using JUnit's
Assert.assertEquals, Assert.assertTrue, and Assert.assertFalse ensures
assertions always execute during test runs.
Add quotes around $ANDROID_HOME/emulator/emulator paths so the macro
handles SDK installations in directories containing spaces.
Move the hardcoded androidx.compose.ui:ui-test-junit4 and
ui-test-manifest dependency strings in build.gradle.kts to named
entries in libs.versions.toml for consistency with the other Compose
library declarations.
Add waitUntilAsserts and waitForNodeWithText extension functions on
AndroidComposeTestRule to replace the repeated waitUntil { runCatching
{ ... }.getOrDefault(false) } pattern that appeared in five places.
… sequence

The previous implementation dispatched five synthetic events
(pointerdown, mousedown, pointerup, mouseup, click) to ensure React
picked up the interaction. In practice, el.click() dispatches a real
click event that bubbles through the DOM, which is sufficient for
React 18's event delegation. This simplifies the JS and removes the
unnecessary PointerEvent/MouseEvent ceremony.
…proach

Replace the dual waiting mechanism (Espresso Web findElement + JS
querySelector) with a single JS-based approach. The Espresso Web
variant used findElement with a getText/webMatches assertion, which
can fail on elements that don't pass Espresso's built-in visibility
check. The JS approach using document.querySelector is simpler and
handles both visible and non-visible elements consistently.

This removes the webMatches, getText, and notNullValue imports that
were only needed by the Espresso-based variant.
Replace the hand-rolled JS that iterates over role="option" elements
to find and click a block by name with Espresso Web's findElement using
an XPath locator. This is more idiomatic and leverages the framework's
built-in element location instead of custom JS string matching.

The dialog wait still uses the JS-based waitForWebViewElement to ensure
the inserter is rendered before attempting the XPath lookup.
The insertBlock method had two inline selectors — a CSS selector for
the inserter dialog and an XPath for matching block options by name.
Extract the CSS selector to INSERTER_DIALOG_SELECTOR (alongside the
other named constants) and the XPath template to an
inserterOptionXpath helper function for clarity.
Centralizes the repeated `onWebView().forceJavascriptEnabled()
.perform(script(js)).get()` boilerplate into a single `runJs()`
method, used by clickViaJs, typeViaExecCommand, readTextViaJs,
and waitForConditionViaJs.
…nViaJs

Avoids silently swallowing JVM errors like OutOfMemoryError or
StackOverflowError during WebView polling.
Replace string concatenation with trimIndent() multiline strings
in clickViaJs, typeViaExecCommand, readTextViaJs, and
waitForWebViewElement for improved readability.
Prevents the emulator boot loop from hanging indefinitely by
failing after 120 seconds (60 iterations x 2s sleep).
…nsRule

When a device has no value for an animation scale setting, adb
returns the string "null". Restoring "null" as the value could
leave animations in an unexpected state. Default to "1.0" instead.
Verify that document.execCommand('insertText') returns true and
throw an AssertionError if it fails, catching silent text insertion
failures.
All callers target Code Editor textarea fields, so the
contenteditable branch (innerText/textContent) is unused.
Simplify to just return el.value.
Store the background emulator PID and kill it if the boot times out,
preventing an orphaned emulator process from lingering after failure.
Introduces a top-level type alias to reduce repetition of the
AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>
type throughout EditorTestHelpers.
Remove clickViaJs and JS-based waitForWebViewElement in favor of
standard Espresso Web findElement/webClick calls. Testing confirmed
that Espresso Web's visibility checks pass for Gutenberg toolbar
elements, making the JS bypasses unnecessary.

The execCommand('insertText') workaround for text input remains, as
Espresso Web's webKeys() genuinely fails on Gutenberg's contenteditable
with "Cannot set the selection end".
@dcalhoun dcalhoun changed the title test: Add Android E2E tests for editor interactions test: add Android E2E tests for editor interactions Feb 26, 2026
@dcalhoun dcalhoun marked this pull request as ready for review February 26, 2026 21:48
@dcalhoun dcalhoun requested a review from adalpari February 26, 2026 21:48
@dcalhoun
Copy link
Member Author

The failing CI checks will be resolved by #338.

Copy link

@adalpari adalpari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 💪

@dcalhoun dcalhoun merged commit 0e2c493 into trunk Feb 27, 2026
12 of 14 checks passed
@dcalhoun dcalhoun deleted the test/android-e2e-tests branch February 27, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants