Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into docs/rules-swift/ as a read-only reference alongside the repo's tailored docs/rules/. PROVENANCE.md records the source, commit, and what was included or excluded; the upstream repo's own VCS, CI, and lint tooling is omitted as machinery rather than rule content. Exclude the vendored tree from the style gate (scripts/check-style.sh) and the pre-commit hook: the canon's enforcement docs quote the forbidden attribution phrases to define them, the same reason the repo's own enforcement files are already excluded.
4.9 KiB
Page Object Model (for UI tests)
Status: MANDATORY for UI tests. A UI test interacts with a screen only through a page object, never through raw element queries. Each screen has one page object that owns its locators and its actions; the test body names pages and calls their methods, and never touches a XCUIElement or a raw identifier string. This keeps tests readable, keeps a UI change confined to one page object, and makes invalid navigation a compile error.
The pieces
-
A base page owns the mechanics. A single shared base class is constructed with the
XCUIApplicationinjected (init(app:)) and holds every reusable interaction: waiting for an element to appear or disappear, tapping, asserting existence, scrolling to an element, taking a screenshot. Every method threadsfile: StaticString = #filePath, line: UInt = #lineso a failure blames the test's call site, not the base page. Methods areopenso a page can override a wait without re-implementing it.A default identifier-driven XCUITest registry ships in
FlowSpecXCUI, part of the FlowSpec package, so an app whose views set accessibility identifiers can run scenarios with no custom registry code; the page objects themselves are per-app. Seeflowspec.md. -
One page object per screen. A concrete page subclasses the base class, file name equals page name. Its locators are computed
XCUIElementproperties that query the app by a shared identifier constant and the typed element kind (app.buttons[ID.Catalog.search],app.staticTexts[ID.Item.title]), never a raw string. Its methods split into verifications (returnSelf) and actions (return the next page). -
A single source of truth for accessibility identifiers. Identifiers live in one shared enum (one nested namespace per screen,
public static letconstants) in a package that BOTH the app and the UI tests depend on. The app sets them (.accessibilityIdentifier(ID.Catalog.search)); the page objects query the same constants. Renaming an identifier is one edit the compiler propagates to producer and consumer, so source and tests cannot drift into a string mismatch. -
Fluent, type-safe navigation. An action returns the page it navigates to (
openItem() -> ItemPage,goBack() -> CatalogPage); a verification returnsSelf. A test reads as a chain (CatalogPage(app:).verifyShown().openItem().verifyShown()), and the compiler rejects an impossible navigation path because the return types do not line up.
DO
- Construct the first page with the app, then chain; let action methods return the next page.
- Put every locator behind a shared accessibility-identifier constant; the app sets it, the page queries it.
- Keep
XCUIElementproperties private on the page; expose screen actions, not elements. - Wait explicitly (
waitForExistence(timeout:)and predicate waits); neversleep(). See../core/no-shortcuts-first-principles.md. - Thread
#filePath/#linethrough waits and asserts so failures point at the test. - One page object per screen; recurring mechanics live once in the base page.
DON'T
- Do not query
app.buttons["raw string"]in a test body; that is the coupling page objects exist to remove. - Do not return
Voidfrom a navigation action; return the destination page so the type system checks the flow. - Do not phrase a locator as a literal; route it through the shared identifier enum.
- Do not
sleep()to dodge a race; wait for the element. A sleep is a silenced symptom. - Do not duplicate wait or tap logic per page; it belongs in the base page.
Acceptance check
A conforming UI test suite has: (1) a shared base class (constructed with the app) that owns waits, taps, and asserts, with #filePath/#line threaded through; (2) one page object per screen, file name equals page name, locators private and routed through a shared identifier enum; (3) a single accessibility-identifier source of truth in a package both the app and tests depend on, with the app setting and the tests querying the same constants; (4) action methods that return the next page and verification methods that return Self; (5) no raw element-query strings and no XCUIElement in any test body (grep the test target); (6) no sleep() anywhere in the page objects or tests. A suite that queries raw strings, or returns Void from navigation, or sleeps to avoid a race, fails this rule even if it currently passes.
Companion rules
flowspec.md: drive page objects from a declarative scenario corpus; the scenario step registry calls page-object actions.pre-ui-layer.md: the accessibility identifiers a page object queries should line up with the surface identities the model exposes, so a scenario can target the same element across renderers.../core/no-shortcuts-first-principles.md: the no-sleep, wait-explicitly discipline.../core/testing-discipline.md: real tests, on every change.