Skip to content

Mobile SDK test hardening, Android E2E in CI, JS bridge consolidation#285

Merged
Alex Freas (akfreas) merged 32 commits into
mainfrom
final-mobile-sdk-fixes
May 27, 2026
Merged

Mobile SDK test hardening, Android E2E in CI, JS bridge consolidation#285
Alex Freas (akfreas) merged 32 commits into
mainfrom
final-mobile-sdk-fixes

Conversation

@akfreas
Copy link
Copy Markdown
Collaborator

@akfreas Alex Freas (akfreas) commented May 25, 2026

Test hardening and CI coverage expansion for the mobile SDKs, plus consolidation of the iOS and Android JS bridges.

  • Consolidate the duplicated ios-jsc-bridge and android-zipline-bridge packages into a single @contentful/optimization-js-bridge under packages/universal that builds one shared source into both native UMD bundles
  • Add Android E2E coverage to main-pipeline.yaml and port the iOS UI test suite over to UI Automator under implementations/android-sdk/uitests
  • Harden Android UI Automator tests against Compose recomposition / accessibility-tree staleness (idle waits, pidof-based forceStop, stale-resilient scroll loops)
  • Fail-fast on the first failing Android E2E test instead of letting the suite grind through every remaining @Before timeout
  • Sort preview-panel audiences by name only (drop the qualified-first tiebreak) so toggling an override no longer demotes the row off-screen mid-scenario
  • Add a preview-refresh button and inline reset-all confirmation to the RN preview panel
  • Various ESLint cleanups in the OptimizationProvider / OptimizedEntry test suites

Alex Freas (akfreas) and others added 24 commits May 22, 2026 13:19
…single @contentful/optimization-js-bridge under packages/universal that builds one shared src/index.ts into both native UMD bundles
…merged @contentful/optimization-js-bridge package
…imization-js-bridge so the JS bridge matches the iOS and Android native SDKs that already call them
…okes it via /usr/bin/sh which errors out with 'Illegal option -o pipefail' on the first line, terminating the script before any apk install or instrument run. The grep checks on the test-output log already detect instrumentation failures regardless of pipe-status propagation
…init renderer at declaration, mark afterEach as void, replace Object.create(null) cast with a type-predicate factory
…thResolvers in createDeferred, void the beforeEach/afterEach calls, init renderer at declaration, replace Object.create(null) cast with a type-predicate factory
…l and 0.4 ratio to named constants, destructure mock.mock.calls all the way through, init renderer at declaration, void the beforeEach/afterEach calls
… test: void the beforeEach call, extract the inner subscriber-notify forEach callback to a named arrow, capture the original destroy via Reflect.get with an explicit this-typed annotation to silence both prefer-destructuring and unbound-method
…eact-web test: add a void-this reference inside the mock destroy method to acknowledge class-methods-use-this, switch indexed array access to computed-key object destructuring
…r tests so they actually validate the mocked destroy/screen/states.eventStream shape at runtime instead of returning true unconditionally; the lint-clean type narrowing now matches what the runtime check claims
…ollView — the e2e Detox suite preview-panel-overrides.test.js was changed in 95d6afc to match the panel scroll container by that id but the SDK side of the rename never landed on final-mobile-sdk-fixes, so all 8 scenarios were failing with 'No views in hierarchy found matching: view.getTag() is preview-panel-scroll'
…eanup-order test that broke the CI Format Check; the cherry-picked lint commit was line-wrapped one way and prettier wants it the other way
…view panel and update Detox scenario 6 to tap by testID
…via sdk.page in the demo app so the override interceptor can be exercised end-to-end from Detox
…and reset taps in PreviewPanelOverridesTests scenario 3, because deactivation drops the audience to the bottom of the sorted list and UiAutomator's findObject returns a stale accessibility node from the now-off-screen position whose ACTION_CLICK silently no-ops on the Compose recomposed segment — verified locally by [bridge-sync] resetAudienceOverride firing after the scroll where previously only overrideAudience fired
…CI x86_64 Compose recomposition surfaces faster than local arm64 by reading every visibleBounds inside a try-catch and falling back to display extents — the stale-handle scenario 3 hit on the previous CI run originated from el.visibleBounds in the scroll loop, not from the click itself
…iebreak in sortAudiences and the mirrored isQualified-first ordering in the RN AudienceSection) so an audience keeps its row when its override flips, removing the spurious accessibility-tree staleness that the test framework hit when a deactivation demoted the audience-toggle off-screen between two taps — buildPreviewModel.test.ts updated to assert the new alphabetical-only ordering, the iOS and Android JS bridge UMD bundles regenerated from the merged source
…verridesTests and refresh the scenario 2 singleClick comment — both were narrowly justified by sortAudiences demoting an overridden audience off-screen between two taps; the panel now sorts by name only and that demotion no longer happens
…us-Compose accessibility-cache pollution that surfaces when multiple scenarios share a single am instrument session — waitAndTap now resolves the element AFTER the 1.5s idle wait (so the captured UiObject2 is not handed across the recomposition window in which Compose invalidates the underlying node id), forceStop polls 'pidof' until the app process is actually gone instead of relying on the kernel-side teardown lagging Thread.sleep(500), and relaunchClean bookends launchApp with two waitForIdle ticks so the new activity's accessibility tree has settled before the next findObject runs
…Guard rules — rename the prose ZiplineContextManager to QuickJsContextManager to match the actual class, update README/AGENTS scope and architecture sections to name QuickJS via io.github.dokar3:quickjs-kt, and drop the defensive -keep class app.cash.zipline.** rule since the module has no Zipline dependency
…ct2.scroll on the inner preview-panel-list scrollable instead of raw device.swipe coordinates — a coordinate swipe past the inner scroll's limit bubbles up to the ModalBottomSheet container as a drag-to-dismiss and tears the panel out of the accessibility tree entirely, observed via diagnostic snapshots showing previewPanel=false mid-scroll, and a semantic scroll on the inner scrollable Compose node stays within it; the loop now does a stale-resilient findObject+visibleBounds (Compose recomposition staled the handle between the two reads and the previous bounds==null code treated that as 'not found' and swiped past the target), accepts partial overlap with the panel viewport instead of demanding full containment (clipped-edge rows are still tappable), and scrolls DOWN then UP if needed so we can recover when the panel entered already past the target — and TestHelpers.waitForElement now includes the selector in its timeout AssertionError so future failures point at the missing selector instead of an opaque message
…trument grind through every remaining @before for its full 20-30s waitForElement timeout — AndroidJUnitRunner has no built-in early-exit, and the CI grep on /tmp/test-output.log for FAILURES only fires after the whole suite finishes, so one root-cause failure in an early test class burned ~25 minutes of GitHub Actions runner time and hit the 45-minute job ceiling before being cancelled; the fix streams am instrument output through an awk filter that, on the first 'Error in test' or 'Process crashed' line, prints the line, runs adb shell am force-stop com.contentful.optimization.uitests to kill the instrumentation host so the remote am instrument -w exits, then exits awk with code 1 to collapse the local pipeline — applied symmetrically in .github/workflows/main-pipeline.yaml and implementations/android-sdk/scripts/run-e2e.sh (the latter gated behind FAIL_FAST=true with an env-var escape hatch for the diagnose-a-later-test case), and the post-run grep now also matches 'Error in test' so the aborted-mid-stream path where the FAILURES summary line is never written still exits non-zero
…ML line because the reactivecircus/android-emulator-runner action's parseScript() in src/script-parser.ts does rawScript.trim().split(/\r\n|\n|\r/) and then runs each non-comment, non-empty resulting line as a separate sh -c — which silently broke the previous multi-line pipeline ('adb shell am instrument ... \' on one line, '| tee ... \' on the next, '| awk ...' on the third) into three independent commands; am instrument ran standalone with its stdout going to the GHA console, the | tee and | awk lines were syntax-error no-ops, /tmp/test-output.log was never written, fail-fast never triggered, and the suite kept running through every later test class's @before timeouts; the new single-line form keeps the tee + awk pipeline intact, and the awk script body uses ;-separated statements instead of newlines so it stays one line
…boot the x86_64 Pixel 7 + api 35 image pays on every job — add an actions/cache@v4 step keyed on api-level/arch/target/profile/device-name pointing at ~/.android/avd plus ~/.android/adb*, run a snapshot-generating emulator-runner step gated on the cache-miss path (headless boot, no audio, no boot animation, -no-snapshot-save deliberately OMITTED so the snapshot gets persisted at shutdown for the cache step to pick up), then flip the actual test-run emulator-runner to force-avd-creation: false and add -no-snapshot-save to its emulator-options so the cached snapshot survives whatever dirty state the suite leaves behind
…te cold boot the x86_64 Pixel 7 + api 35 image pays on every job — add an actions/cache@v4 step keyed on api-level/arch/target/profile/device-name pointing at ~/.android/avd plus ~/.android/adb*, run a snapshot-generating emulator-runner step gated on the cache-miss path (headless boot, no audio, no boot animation, -no-snapshot-save deliberately OMITTED so the snapshot gets persisted at shutdown for the cache step to pick up), then flip the actual test-run emulator-runner to force-avd-creation: false and add -no-snapshot-save to its emulator-options so the cached snapshot survives whatever dirty state the suite leaves behind"

This reverts commit d85979e.
@akfreas Alex Freas (akfreas) marked this pull request as ready for review May 27, 2026 07:44
@akfreas Alex Freas (akfreas) changed the title Final mobile sdk fixes Mobile SDK test hardening, Android E2E in CI, JS bridge consolidation May 27, 2026
…mechanics.md to its main-branch state — commit 95d6afc on this branch unintentionally reverted PR #275's doc updates (renames entry→baselineEntry, threshold→minVisibleRatio, viewTimeMs→dwellTimeMs, plus removal of the still-live onStatesReady row from the OptimizationProvider props table) while keeping the corresponding code-side renames intact, leaving the doc describing prop names that no longer exist on OptimizedEntry and omitting an OptimizationProvider callback that is still wired through OptimizationProvider.tsx and OptimizationRoot.tsx; restored via git checkout 9aefd58 -- <path> so the file matches main exactly and git diff main on this path is now empty
Comment thread .github/workflows/notify-slack.yml
Comment thread eslint.config.ts
… commit 95d6afc on this branch had bolted on an unrelated guides/concepts notification rewrite (new has_guides/has_concepts classifier branches, a fanned-out 7-case message_title/message_body matrix, and a 'guide notifications' tweak to the SLACK_TECH_WRITER_ID error message) alongside a stale packages/ios/ios-jsc-bridge/* → packages/universal/optimization-js-bridge/* rename that anticipated the bridge consolidation but hadn't been validated against any of the recent main-branch updates to this workflow, leaving the file mixing brand-new additions with reapplied older content; the bridge-package notification entry will land separately with the actual bridge consolidation PR so the workflow on this branch is now identical to main with git diff main on this path empty
…o react-web-sdk provider test workarounds it forced — commit 95d6afc had dropped `**/*.test.tsx`/`**/*.spec.tsx`/`**/test/**/*.tsx`/`**/e2e/**/*.tsx` from the vitest-test-files glob (and added a now-redundant `import { URL } from 'node:url'` plus an inlined `new URL('.', import.meta.url).pathname` form) so the test-rule relaxations (no-floating-promises, no-magic-numbers, init-declarations, no-unsafe-* on member-access/argument/assignment, etc.) stopped applying to .tsx test files, which is what forced the `void beforeEach(…)` / `eventSubscribers.forEach(notifySubscriber)` extraction / `Reflect.get(ContentfulOptimization.prototype, 'destroy')` cast / `void this` no-op / `const { [index]: config } = constructedConfigs` destructure scattered through OptimizationProvider.onStatesReady.test.tsx and OptimizationProvider.trackEntryInteraction.test.tsx; with the test-glob restored to its main-branch form (.ts + .tsx for src/test/e2e) and the URL import + `const url: URL = new URL(...)` form back, those workarounds become unnecessary and both test files are restored byte-for-byte to main while eslint.config.ts keeps only the new `**/optimization-js-bridge/**` ignore line motivated by this PR's bridge consolidation (the universal-tree bridge package isn't covered by the pre-existing `**/ios/**`/`**/android/**` ignores its predecessor packages.ios.ios-jsc-bridge and packages.android.android-zipline-bridge fell under)
@akfreas Alex Freas (akfreas) merged commit b131db3 into main May 27, 2026
35 checks passed
@akfreas Alex Freas (akfreas) deleted the final-mobile-sdk-fixes branch May 27, 2026 14:52
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.

2 participants