Skip to main content

appium-page-objects

Why It Exists

Before this skill family, no RN/Appium testing skill existed at all: a grep across ~/.claude/skills and ~/.agents/skills for testid|appium|webdriver|wdio turned up 17 matched files, every one of them belonging to the Playwright family, Chrome DevTools tooling, or next-frontend-quality-loop — zero hits in either vercel-react-native-skills or mobile-expo-app, the two existing RN-adjacent skills. Meanwhile a companion command plan, cars next-appium, had already hard-coded appium-page-objects by name in generated story prose and Rust unit tests. .omc/plans/appium-rn-testing-skills.md (local research artifact, not tracked in this repo) authored four skills — appium-testid-attributes, appium-testid-catalog, appium-attribute-waits, and this one — to fill that gap, mirroring the existing playwright-* family's producer/catalog/consumer/structure split.

The differentiated content in this specific skill traces back to a real defect found mid-review, not a hypothetical one. swarmdriver's nativeUtils.findEle() is published as Promise<any>, and the plan's Critic (codex) round-3 pass found that any is load-bearing, not sloppy: findEle internally awaits $(builtSelector) but returns false on its not-found and error branches, so a getter that merely annotates findEle(...)'s return as ChainablePromiseElement is an unsound cast — the lie resurfaces later as TypeError: el.click is not a function at a call site the type checker had already blessed. Critic round 4 then verified the fix for real, not just on paper: codex reinstalled the dependency set and compiled a representative getter using the sound alternative — calling $(nativeUtils.buildSelector(bareId)) directly and typing it with the real, exported ChainablePromiseElement from webdriverio — and confirmed it typechecks cleanly.

A second concrete correction shaped invariant 3: the plan's original draft had the testID convention backwards, proposing ~-prefixed selectors. Round-1 Architect review traced this against swarmdriver's actual buildSelector() (utils.ts:124-139) and found a ~ prefix resolves to WDIO's accessibility id strategy, which returns the selector untranslated — silently dropping the Android <appPackage>:id/ prefix buildSelector would otherwise add. Catalogs and page objects were corrected to store and pass bare testIDs only. The introducing commit (5a4e3d3) also notes the Ralph implementation pass caught a wrong deep-link scheme in example code and a typecheck-and-behavioral-test harness that didn't assert its own emitted file count or actually run its own tests — both fixed before merge.

What It Does

appium-page-objects defines the output shape of an Appium/WDIO page object for a React Native screen: the base class contract, how a locator is built and typed, the wait discipline around interactions, the one-file-per-screen naming rule, and the boundary between screen structure and cross-screen business flow. It does not define the wait-helper implementations themselves (that's appium-attribute-waits) or the typed testID catalog mechanics (that's appium-testid-catalog) — this skill only consumes what those two publish.

Every page object extends NativeBase (swarmdriver's packages/core/src/helpers/native/base.ts, re-exported through helpers/native/index.ts and packages/core/src/index.ts), passes the screen's root testID to super(), exposes locators as get accessors, asserts internally via verify* methods, and keeps multi-screen flows out of any single page object's methods.

How To Use It

Triggers on: "new Appium page object", "extend NativeBase", "page object structure for RN screen", "expand a stub page object", "one page object per screen", "verify method pattern appium", "business logic leaking into an Appium page object".

skills add git@github.com:catesandrew/next-starters.git --skill skills/appium-page-objects -g
npm install @next-starters/skill-appium-page-objects
/plugin marketplace add catesandrew/next-starters
/plugin install appium-page-objects@next-starters

Gotchas & Invariants

  • Import the base class as NativeBase, never as BaseBase is base.ts's file-local default export name and is not importable from anywhere a consumer can reach.
  • A locator is a get accessor returning $(nativeUtils.buildSelector(bareTestId)), typed ChainablePromiseElement from webdriverio — never nativeUtils.findEle() with a return annotation bolted on; findEle is Promise<any>-shaped because it can genuinely resolve false, and it's async besides, so it can't work as a getter regardless.
  • Every testID comes from the typed catalog, bare — never hand-written and never ~-prefixed. A ~ prefix makes buildSelector return the selector untranslated, which silently drops the Android <appPackage>:id/ resource-id prefix.
  • buildSelector reads the bare appPackage capability key on Android (utils.ts:132), while the module's own reference comment elsewhere uses the W3C-namespaced appium:appPackage (utils.ts:168). Under a strict W3C session that only carries the namespaced key, every locator silently becomes id=undefined:id/<testID>. Assert driver.capabilities.appPackage is a non-empty string at session start and fail loudly instead of treating undefined:id/ as expected.
  • nativeUtils.findEles discards buildSelector's translated output (utils.ts:293 vs :299) — a known swarmdriver defect, not a pattern to route a plural/list lookup through.
  • verify* methods assert internally with expect-webdriverio's expect(...) and return void — never a boolean for the caller to assert on, and never a bespoke assertion helper.
  • Action methods compose the sibling skill's inherited waits (waitForIsShown()/waitForIsNotShown()) before and after an interaction — never a bare driver.pause() without an inline comment justifying the exact millisecond budget.
  • One page-object file per screen/route, named by the deterministic route-to-class rule (widgets/createWidgetsCreatePage in pageobjects/widgets/WidgetsCreatePage.ts). A modal or bottom sheet with its own root testID gets its own file.
  • Page objects encode screen structure, not business/domain flow — a flow spanning screens composes several page objects one layer up, in the spec or a flow module.
  • appium-attribute-waits — owns the NativeBase wait semantics and driver.waitUntil() composition this skill's action methods call into but never redefine.
  • appium-testid-attributes — instruments the RN components with the testID/accessibility props this skill's locators consume.
  • appium-testid-catalog — owns the typed bare testID registry every locator getter pulls from, plus the locator-translation and platform-divergence authority.

Sourced from: skills/appium-page-objects/metadata.json, skills/appium-page-objects/SKILL.md, .omc/plans/appium-rn-testing-skills.md (local research artifact, not tracked in this repo), git log --follow -- skills/appium-page-objects (commit 5a4e3d3)