test: add Android E2E tests for editor interactions#340
Merged
Conversation
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.
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.
6bffae8 to
567c7c7
Compare
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".
Member
Author
|
The failing CI checks will be resolved by #338. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 iOSEditorUITestHelpers.swift) with methods for navigation, text input, block insertion, mode switching, content reading, and waiting.EditorInteractionTest.kt— Two test cases matching iOS:testEditorWebViewBecomesVisible— Navigates to the editor and verifies the WebView loads.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 viaUiAutomationshell commands.libs.versions.tomlandapp/build.gradle.kts.test-android-e2eandtest-android-e2e-devwith auto-boot emulator logic (ENSURE_ANDROID_DEVICEmacro).Note
CI integration is not included in this PR. The Buildkite
androidqueue 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
make test-android-e2e— both tests should pass (100%, ~13s).make dev-serverthenmake test-android-e2e-devfor dev mode.Accessibility Testing Instructions
N/A — these are automated tests, not UI changes.