feat(hierarchy): flat-to-tree + path query selector (#41)

* 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.
This commit is contained in:
pj authored and GitHub committed 2026-04-25 18:46:15 +07:00
1 parent 2a1b263b8c
commit 6c32fb0e1d
11 files changed
+309 -41

No files matched your search

@@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import app.folio.data.Repository
import app.folio.navigation.Navigator
@@ -59,6 +60,7 @@ fun AddAccountScreen() {
}
Screen(
modifier = Modifier.testTag("AddAccountScreen"),
header = {
Header(title = "New account", left = { BackButton(onClick = { Navigator.back(Route.Home) }) })
},
@@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import app.folio.data.Repository
@@ -59,7 +60,7 @@ fun LoginScreen(onLoggedIn: (String) -> Unit) {
onLoggedIn(user)
}
Screen {
Screen(modifier = Modifier.testTag("LoginScreen")) {
Spacer(Modifier.height(16.dp))
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
@@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
@@ -49,6 +50,7 @@ fun HomeScreen(user: String, onLogout: () -> Unit) {
val total = balanceOf(txns)
Screen(
modifier = Modifier.testTag("HomeScreen"),
header = {
Header(
title = "Accounts",
@@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -90,6 +91,7 @@ fun AddTransactionScreen(accountId: String) {
}
Screen(
modifier = Modifier.testTag("AddTransactionScreen"),
header = {
Header(
title = "Add transaction",
@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@@ -74,6 +75,7 @@ fun LedgerScreen(accountId: String) {
val balance = balanceOf(txns)
Screen(
modifier = Modifier.testTag("LedgerScreen"),
header = {
Header(
title = account.name,
@@ -19,12 +19,13 @@ import app.folio.ui.theme.ScreenPad
@Composable
fun Screen(
modifier: Modifier = Modifier,
header: @Composable (() -> Unit)? = null,
footer: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
val t = LocalTokens.current
Column(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize().then(modifier)) {
if (header != null) header()
Column(
modifier = Modifier