feat(mobile): match Android test ids without their package prefix

Merged
MI
9d ago
+195-18

What and why

I was adding a .NET MAUI example (it comes in a follow-up PR) and the Flutter tests, copied over unchanged, passed on iOS and found nothing on Android. MAUI writes AutomationId into the resource id as <package>:id/<AutomationId> (SemanticExtensions.cs#L98), the shape of a classic android:id from an XML layout, which Android reports with the package of the resource table. Compose (with testTagsAsResourceId), React Native and Flutter write the bare string, and those are the only Android stacks in the examples and benchmarks, so getByTestId never had to deal with a prefix.

  • On Android, getByTestId('greet') now also matches <package>:id/greet for any package but the framework's android (ANDROID_RESOURCE_ID in packages/mobile/src/locate.ts), the way Maestro's idMatches does. That covers builds whose resource package differs from the application id (applicationIdSuffix, flavors).
  • The full id still matches, and a RegExp is tested against both forms. iOS, the projected tree and the cache's id= selectors don't change.
  • SemanticNode.testId and the getByTestId JSDoc, with their copies in docs/reference/engine.mdx and docs/reference/screen.mdx, now say an engine may also match an id without its platform's namespacing. The Android rule is in docs/reference/mobile.mdx. No type changes; e2e gets a patch changeset for the doc comments.

Two things change for existing Android suites, and the @e2e-dev/mobile changeset says so. A negative check on a bare id like toHaveCount(0) used to pass because nothing matched, and now it sees the node. And if one screen has both greet and <package>:id/greet, the string greet is ambiguous, so the runner's strictness check refuses it.

Verified

Ran it locally: yes

The MAUI example from the follow-up PR on an Android 15 emulator and an iOS 26.4 simulator, with @e2e-dev/mobile packed from main and from this branch and installed into it with npm install --no-save.

Example locator testsmainthis branch
Android0/3, locator matched no nodes within getByTestId("greet")3/3
iOS3/33/3

Unit tests cover an applicationIdSuffix build (app com.foo.debug, view com.foo:id/greet), framework ids staying full-only, and a bare id next to a prefixed one returning both nodes.

I didn't run bench:ab, since it only covers the web suites and this is one string comparison in the mobile locate. The mobile benchmark shouldn't move either. The prefixed ids in its Android snapshots are dev.e2e.benchmark:id/action_bar_root and the status bar's com.android.systemui:id/*, and no test queries any of their short names.

Checklist

  • Changeset added (pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only.
  • Breaking change: title carries !, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md).
  • Docs updated in the same PR: the docs/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures. (Doc comments only, no type or wire change.)
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile. (Only the SemanticNode.testId comment changed; @e2e-dev/web behaves the same, so it has no changeset.)