Files
sanderling/examples/folio
pj 34fb73d6cb fix(folio): stop abusing contentDescription as data carrier (#45)
* feat(hierarchy): full-attribute selector system

- Add Attributes map to Element (raw platform attrs + serialized booleans)
- Add Selector / AttrFilter types for multi-filter AND matching
- Add matchAttr with alias expansion and substring/boolean semantics
- Add matchSelector (AND of all filters)
- id: and desc: keep exact/suffix/prefix semantics for backward compat
- text: widens to substring via matchAttr
- default: case routes unknown kinds to matchAttr (NEW)
- Add Tree.FindNode / Tree.FindAllNodes returning *Node
- Add Node.Find / Node.FindAll for scoped subtree string search
- Add Node.FindBySelector / Node.FindAllBySelector for object AND search
- Add attributeAliases for cross-platform name expansion

* test(hierarchy): full-attribute selector coverage

- raw resource-id: substring match
- label:/content-desc: alias expansion to accessibilityText on iOS
- scrollable:true/false boolean exact match
- title: iOS-only attribute, graceful nil on Android
- text: substring widening
- Selector AND: both filters must match; single miss returns nil
- Node.Find scoped search: descendants only, not siblings

* feat(verifier): object-form selectors + attrs + element-level find

- ax.find/findAll accept string or {attr:value} JS objects
- Object form builds Selector with AND semantics
- Returned element objects expose attrs sub-object (raw platform attrs)
- Returned element objects expose .find() and .findAll() scoped to subtree
- Element-level .find/.findAll accept string or object selectors

* feat(spec): extend AccessibilityElement and AccessibilityTree types

- AccessibilityElement gains attrs, find(), findAll()
- find/findAll on both Tree and Element accept string | AttrSelector
- AttrSelector = Record<string, string> for object-form AND matching

* feat(folio): migrate to chained object-form selectors

- Replace path queries (desc:X > desc:Y) with chained API
- Screen root lookups use { accessibilityText: "ScreenName" }
- Element-scoped searches use find/findAll with string or object
- Keep string selectors for descPrefix: and desc:Back (shows both forms)

* fix(folio): use account_card:id desc, expose balance via text semantics

* fix(folio): embed accountId in LedgerScreen desc, expose values via text semantics

* fix(spec): replace desc-parsing with text-based parseDollarCents extraction
2026-04-25 23:43:19 +07:00
..
2026-04-20 16:04:20 +07:00
2026-04-25 20:04:29 +07:00

Folio

A minimal Kotlin Multiplatform personal-ledger app: login with demo credentials, create accounts, add credits and debits. Shared UI across Android, iOS, and Web via Compose Multiplatform. Doubles as the example sanderling runs its property-based specs against.

Stack

  • Kotlin Multiplatform + Compose Multiplatform (shared UI)
  • kotlinx.serialization for file-backed persistence
  • kotlinx.coroutines for state flows

Prerequisites

  • just
  • JDK 17
  • Android SDK (auto-discovered under $ANDROID_HOME, ~/Library/Android/sdk, or the Homebrew cask)
  • Xcode 16+ and xcodegen (brew install xcodegen) for iOS

Android

just install      # build + install on a booted emulator / device
just uninstall
just clean

iOS

just ios                          # default device: iPhone 17 Pro
IOS_DEVICE="iPhone 15" just ios   # pick a different simulator

just ios regenerates iosApp/iosApp.xcodeproj from iosApp/project.yml, builds the KMP framework, links it into the SwiftUI host, installs, and launches.

Demo credentials

email:    [email protected]
password: ledger123

Run a sanderling test (Android)

just test

If no device is connected, sanderling boots the single AVD it finds. With multiple AVDs, pick one:

AVD=Pixel_7 just test

Persistent settings can live in .env alongside the justfile:

AVD=Pixel_7
DURATION=5m

Traces land in ./sanderling/runs/<timestamp>/.

How it connects to sanderling

  • UI elements expose state via content descriptions (account:$id:$balance, ledger_row:$id:$signed, ledger_balance:$cents, active_account:$id, focused_input:$field)
  • sanderling/spec.ts imports @sanderling/spec, reads state via s.ax.*, asserts properties, and weights the actions the fuzzer picks from
  • just test invokes sanderling test against the installed APK