* feat(hierarchy): add Node tree + path query support
Preserve parent-child relationships in a Node tree at parse time.
Extend Find/FindAll with " > " path operator for scoped queries,
e.g. id:LoginScreen > desc:EmailInput.
Flat Elements slice and all public signatures unchanged.
* test(hierarchy): add path query tests
Cover single-level, multi-level, mixed-type, not-found, wrong-subtree,
and FindAll across multiple roots.
* feat(folio): add modifier param to Screen composable
* feat(folio): testTag LoginScreen
* feat(folio): testTag HomeScreen
* feat(folio): testTag AddAccountScreen
* feat(folio): testTag LedgerScreen
* feat(folio): testTag AddTransactionScreen
* feat(folio): scope spec selectors to screen testTags via path queries
* fix(sidecar): align grpc-netty/grpc-okhttp with grpc-core version
Maestro 1.40.0 brings grpc-netty:1.50.2 which compiled against
AbstractManagedChannelImplBuilder, removed in grpc-core 1.64+.
Gradle was upgrading grpc-core to 1.68.0 while leaving grpc-netty at
1.50.2, causing NoClassDefFoundError at startup.
Force grpc-netty and grpc-okhttp to match grpcVersion so all gRPC
artifacts are binary-compatible.
* fix(sidecar): use ephemeral host port for AndroidDriver
Hard-coded port 7001 caused TcpForwarder.start() to fail with
TimeoutException when a previous sidecar process held the port open.
Pick a free port via ServerSocket(0) instead.
* chore(sidecar): bump maestro to 2.4.0, exclude graalvm from fat JAR
Maestro 2.4.0 still ships grpc-netty:1.50.2 so the grpc transport
resolution strategy is kept. GraalVM JS is excluded: unused by our
sidecar and causes shadow JAR expansion errors (pom treated as zip).
* fix(sidecar): update AndroidDriver calls for Maestro 2.4.0 API
launchApp no longer accepts a UUID argument. Third constructor param
changed from hostname to emulatorName so drop the "localhost" value.
* feat(ios): add simulator management package
* feat(testrun): add iOS platform path (simctl launch + direct TCP)
* feat(cli): add --ios-device flag and IosDevice option
* feat(sdk-ios): add Kotlin Native iOS SDK (TCP agent + POSIX socket + dispatch pauser)
* feat(folio-ios): wire SanderlingIos.start() in MainViewController
* feat(folio-ios): add test-ios justfile recipe
* fix(sdk-ios): remove unavailable C macros; manual byte swap + no-cast warnings
* fix(testrun): simctl-first launch order for iOS; Maestro init after SDK connects
* feat(proto): add env map to LaunchRequest
* feat(driver): add env param to Launch interface + all implementations
* feat(testrun): launch iOS app via XCTest with env vars instead of simctl
* feat(sidecar): add IosDriverBackend using Maestro IOSDriver + env pass-through
* feat(sidecar): wire env map in DriverService + IosDriverBackend in Main
* fix(sidecar): use LocalIOSDevice (WDA+simctl) + stop before relaunch
* fix(sidecar): include exception type in gRPC error description
* fix(sidecar): pick free WDA port instead of hardcoded 9100
Use SocketUtils.nextFreePort to pick a free port in the 22000-23000 range
rather than hardcoding 9100, which only worked if a previous WDA session
left a listener there.
* fix(sdk-ios): check semaphore wait result and throw on snapshot timeout
dispatch_semaphore_wait returns nonzero on timeout; ignoring the return
value caused pauseAndSnapshot to silently return an empty map, sending a
garbage empty STATE frame to the host. Now throws so the agent loop
reconnects instead.
* fix(folio-ios): register snapshot extractors before starting agent
SanderlingIos.start() was called before the snapshot objects were
initialized, so a PAUSE message arriving early produced an empty snapshot.
Move start() to after all extractors are registered.
* chore(ios): remove dead LaunchApp function
LaunchApp had no callers since bff3a49 switched iOS launch to go through
the sidecar driver. Remove the dead code and unused os import.
* test(ios): add unit tests for pickSimulator and iOS flag parsing
Tests for all pickSimulator branches (by name, by UDID, unknown, empty
list, iPhone preference, fallback to first). Also tests BootedUDID on a
canceled context and verifies --platform ios and --ios-device flags are
accepted by parseTestArgs.
* fix(ios): propagate error from BootedUDID instead of silently swallowing
* fix(sidecar): IosDriverBackend.healthy() returns true; WDA liveness checked in open()
* fix(sidecar): warm up WDA after health check to absorb startup race
* fix(runner): surface clear message on WDA connection drop
* docs(testrun): note WDA warmup location above WaitForHealth
* fix(sidecar): extract warmup + add one-shot WDA reconnect on IOException
* fix(runner): fatal on permanent WDA drop during hierarchy fetch
* fix(sidecar): walk cause chain in withReconnect to catch Maestro-wrapped IOException
* fix(sidecar): explicit Unit return in pressKey and waitForIdle withReconnect lambdas
* fix(sidecar): serialize WDA reconnect with ReentrantLock to prevent concurrent xcodebuild races
* fix web examples package config
* Revert "fix web examples package config"
This reverts commit 70c10ade27.
* chore: gitignore built sanderling binary
* fix(hierarchy): parse iOS [x1,y1][x2,y2] bounds + match iOS merged desc labels
* refactor(folio-spec): replace bloated spec with two focused properties
Login is opportunistic. Two concrete properties:
1. every new account starts with balance 0
2. every new txn changes ledger balance by exactly its signed amount
Actions: directed login -> addAccount -> addTxn -> back weighted flow.
* feat(proto): add env map to LaunchRequest
* feat(driver): add env param to Launch interface and all implementations
* feat(sidecar): add IosDriverBackend using Maestro IOSDriver + env pass-through
* feat(testrun): launch iOS app via XCTest with env vars instead of simctl
* fix(ios): replace LaunchApp with BootedUDID; simctl launch moved to XCTest path
* test(cli): add tests for ios platform flag and ios-device flag parsing
* fix(sdk-ios): check semaphore wait result; resolve port from args and env; register extractors before start
* test(folio): replace tautological properties with 4 domain invariants
Drop properties that can't fail (e.g. List.size >= 0, balances are Long
integers) or that just check the extractor itself (account_count equals
accounts.length). Keep auth routing liveness, error-clear liveness, and
reachability goals.
Add four properties that target the real code paths:
- balanceMatchesTransactionDelta: a new ledger row shifts the balance
by exactly its signed amount (credit +, debit -).
- totalEqualsSumOfAccounts: home total equals sum of per-account
balances at every state, not only on home.
- balanceChangeRequiresActiveAccount: an account's balance can only
change while that account is the active one in the navigator.
- duplicateAccountNamesRejected: account names are unique under the
app's actual dedup rule (case-insensitive after trim).
* chore(folio): upgrade sdk-android to io.github.priyanshujain.sanderling:0.0.1-rc4
Group id moved from io.github.priyanshujain to
io.github.priyanshujain.sanderling in the rc4 publish.
* chore(folio): pin @sanderling/spec to 0.0.1-rc4
* refactor(sdk-android): publish under io.github.priyanshujain.sanderling
Move the published Maven coordinates to a sanderling sub-namespace so the
brand is visible in the dependency line (was io.github.priyanshujain:sdk-android).
Sub-namespace is auto-allowed by Sonatype under the verified parent groupId.
* chore: update sdk-android coordinates in folio + docs
Follow the groupId change to io.github.priyanshujain.sanderling:sdk-android.
* ci(docs): use --version flag for d2 install script
The d2 installer accepts --version vX.Y.Z, not --tag. The --tag form
was rejected as "unrecognized flag" on the docs workflow run after
PR #26 merged.
* refactor: rename examples/sample-app to examples/folio
Directory-level rename and path references in Go tests, bundle-check,
top-level README, and getting-started docs. Package declarations,
Gradle config, iOS bundle IDs, and class names follow in later commits.
* refactor(folio): rename Kotlin package dev.uatu.sample to app.folio
Moves source dirs and sqldelight schema from dev/uatu/sample to
app/folio, updates package declarations and imports, and switches
Android namespace/applicationId, iOS binaryOption bundleId, and
sqldelight database packageName to the new identifier.
* refactor(folio): rename SampleApplication to FolioApplication
Android manifest now points at .FolioApplication with label 'Folio'
instead of 'Uatu Sample'.
* refactor(folio): set iOS bundle id and display name to Folio
bundleIdPrefix + PRODUCT_BUNDLE_IDENTIFIER -> app.folio.
CFBundleName + CFBundleDisplayName -> 'Folio'.
* refactor(folio): update demo email to [email protected]
* refactor(folio): point justfile at app.folio bundle id
Updates xcrun simctl launch target, uatu test --bundle-id, and the
build/uninstall comments to reference folio instead of sample.
* test: update fixture package ids to app.folio
Sidecar activity-resolver test and verifier spec-integration XML
fixtures referenced the old dev.uatu.sample Android package. Updates
them to match the folio app's real package id so the tests stay
representative of what the CLI sees on-device.
* test(verifier): rename SampleApp identifiers to Folio
Renames TestSampleAppSpec* functions, bundleSampleAppSpec helper, and
sampleAppHierarchyXML const (now loginHierarchyXML for consistency with
the other per-screen fixtures). Updates trailing sample-app mentions in
comments and assertion messages.
* refactor(folio): rename Gradle/npm/wasm project identifiers to folio
settings.gradle.kts rootProject.name, package.json + package-lock.json
name, and the WasmJS index.html <title> all still read 'uatu-sample' /
'Uatu Sample'. Realigns them with the Folio brand.
* docs(folio): rewrite README title + getting-started bundle id
examples/folio/README.md is now titled 'Folio' with the Kotlin source
paths corrected to app/folio. Getting-started example uses --bundle-id
app.folio. Harness launch message is now generic ('app under test')
since uatu-sample-harness is not specific to folio.
* chore(folio): drop trailing 'sample' reference in gradle.properties
* refactor(folio): rename LoginPage composable to LoginScreen
Align with KMP/Android industry convention (NowInAndroid, Cash App,
JetBrains samples use Screen, not Page).
* refactor(folio): rename HomePage composable to HomeScreen
* refactor(folio): rename AddAccountPage composable to AddAccountScreen
* refactor(folio): rename LedgerPage composable to LedgerScreen
* refactor(folio): rename AddTransactionPage composable to AddTransactionScreen
* refactor(folio): split Models.kt into app.folio.data package
Account, Transaction (with TxnType), and Session move into their own
files under app.folio.data, matching NowInAndroid-style per-type
organization.
* refactor(folio): move data layer into app.folio.data package
Repository, LedgerStore (expect + interface), SqlLedgerStore,
WebLedgerStore, DriverFactory (expect + actuals), AndroidLedgerContext,
and Snapshot move into app.folio.data. Update all consumer imports.
* refactor(folio): move Navigation into app.folio.navigation package
Split the former Navigation.kt into Route.kt (sealed interface) and
Navigator.kt (singleton). Update consumer imports across screens,
App.kt, and FolioApplication.
* refactor(folio): move Platform and Format into app.folio.platform
Both files carry expect declarations (Platform object, formatDate);
grouping them into a dedicated platform package makes the KMP seam
obvious and mirrors the structure used by JetBrains samples.
* refactor(folio): move login into feature/auth package
Create app.folio.feature.auth with LoginScreen + LoginUiState. Inline
the former Auth.kt (DEMO_EMAIL, DEMO_PASSWORD, checkCredentials) into
LoginScreen since it is the sole caller.
* refactor(folio): move HomeScreen into feature/home package
* refactor(folio): move account creation into feature/account package
AddAccountScreen gets its own AddAccountUiState colocated with the
screen, replacing the shared UiState.addAccountError.
* refactor(folio): move ledger screens into feature/ledger package
LedgerScreen and AddTransactionScreen move into app.folio.feature.ledger
with AddTransactionUiState (txnError, txnFormType) colocated. The
former catch-all UiState.kt is removed now that each screen owns its
state alongside its UI.
* refactor(folio): split Theme.kt; move theme and icons to subpackages
Theme split into Theme.kt (tokens, layout dims, LedgerTheme) and
Type.kt (typography) under app.folio.ui.theme. Icons moves to
app.folio.ui.icon. Update every consumer's imports to match.
* refactor(folio): split ui components into per-file under ui/component
Former Widgets.kt and Components.kt become 10 focused files: AppButton,
Card, EmptyState, ErrorText, FieldLabel, Header, IconButton (w/
BackButton), Screen, Segmented, TextInput. Matches NowInAndroid style
of one composable per file in a designsystem/component package.
* chore(folio): consolidate uatu testing files under uatu/ folder
Move spec.ts, package.json, package-lock.json into examples/folio/uatu
so all uatu-specific testing artifacts live in one place. runs/ and
node_modules/ follow the same convention (both remain gitignored).
Update justfile, README, and the two Go consumers (bundle-check tool +
verifier/trace tests) that referenced the old path.
* refactor(trace): drop folio path in writer test
Round-trip only needs a non-empty string; neutralize to keep the
library free of folio references.
* refactor(sidecar): neutralize ResolveActivity test fixtures
Swap app.folio for com.example.app in the fixture strings so the
sidecar tests don't reference the example app by name.
* refactor(bundle-check): take spec path as argument
Previously the tool hardcoded examples/folio/uatu/spec.ts. Accept a
positional spec path instead so the tool works for any example and
leaves no folio reference in the library surface.
* test(verifier): add neutral integration spec and hierarchy fixtures
Adds testdata/integration_spec.ts with two routes ("list", "form"),
an InputText on text_field, a Tap on primary/secondary_action, a
safety property (itemCountNonNegative), and a liveness property
(submitEventually). Adds hierarchies_test.go with matching XML
fixtures. Constants intentionally go in a _test.go at package root
rather than testdata/hierarchies.go because go skips .go files
under testdata/.
* refactor(verifier): replace folio integration tests with neutral ones
Renames bundleFolioSpec -> bundleIntegrationSpec and the three Test*
entry points to TestIntegrationSpec*. Uses the synthetic spec and
hierarchies added in the previous commit so the library's test suite
no longer references examples/folio at all.
Folio-specific coverage remains covered by examples/folio/justfile's
'just test' which exercises the real spec on device/emulator.
* chore: remove cmd/uatu-sample-harness
Not referenced by Makefile, docs, CI, or any script. Duplicates the
adb reverse helpers already in cmd/uatu/test_run.go, and its name
implies ownership by the sample app which violates the library/
example decoupling. If a bare-protocol debugging tool is later
needed it belongs inside cmd/uatu/.
* docs(folio): drop Layout section and KMP layout paragraph; fix AVD override syntax
The directory-tree Layout section rots faster than the code and
duplicates what ls shows for free. The expect/actual paragraph in
Stack was the same kind of filler. The README also claimed 'just
AVD=Pixel_7 test' but the justfile reads AVD as an env var via
env_var_or_default, so the correct invocation is 'AVD=Pixel_7
just test'.