From be8a7e019449fd0a9049a73f8320a18732a30150 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 19 Aug 2026 18:37:51 +0530 Subject: [PATCH] feat(hierarchy): name the route a native tree shows The screen name was web-only: the Chrome driver stamps sanderling-screen on the root and nothing else does, so every Android and iOS step recorded and logged an empty screen. The route marker the tree already carries (the resource id ending in Screen, the same one Transitional counts) names it. --- internal/hierarchy/hierarchy.go | 25 +++++++++++++ internal/hierarchy/hierarchy_test.go | 54 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/internal/hierarchy/hierarchy.go b/internal/hierarchy/hierarchy.go index 02ba6d8..b19b88e 100644 --- a/internal/hierarchy/hierarchy.go +++ b/internal/hierarchy/hierarchy.go @@ -667,6 +667,31 @@ func (t *Tree) Transitional() bool { return false } +// ScreenName names the route this tree shows: the driver-set screen when the +// platform reports one (web), otherwise the resource id ending in "Screen" that +// marks the route composable. A tree carrying two different route ids is a +// cross-fade in flight and names no screen, which is the same reading +// Transitional gives it. +func (t *Tree) ScreenName() string { + if t == nil || len(t.Elements) == 0 { + return "" + } + if screen := t.Elements[0].Screen; screen != "" { + return screen + } + name := "" + for _, element := range t.Elements { + if !strings.HasSuffix(element.ResourceID, "Screen") { + continue + } + if name != "" && name != element.ResourceID { + return "" + } + name = element.ResourceID + } + return name +} + // Find returns the first element matching the selector, or nil. func (t *Tree) Find(selector string) *Element { node := t.FindNode(selector) diff --git a/internal/hierarchy/hierarchy_test.go b/internal/hierarchy/hierarchy_test.go index 02e6bd5..d742bfc 100644 --- a/internal/hierarchy/hierarchy_test.go +++ b/internal/hierarchy/hierarchy_test.go @@ -1821,3 +1821,57 @@ func TestTagMatchesTheElementItNames(t *testing.T) { }) } } + +func TestScreenNameNamesTheRouteTheTreeShows(t *testing.T) { + cases := []struct { + name string + tree string + want string + }{ + { + "driver-set screen", + `{"attributes": {"bounds": "[0,0,10,10]", "sanderling-screen": "/ledger"}, "children": []}`, + "/ledger", + }, + { + "route marker", + `{"attributes": {"bounds": "[0,0,10,10]"}, "children": [ + {"attributes": {"resource-id": "HomeScreen", "bounds": "[0,0,10,10]"}, "children": []} + ]}`, + "HomeScreen", + }, + { + "route marker repeated by a nested node", + `{"attributes": {"bounds": "[0,0,10,10]"}, "children": [ + {"attributes": {"resource-id": "HomeScreen", "bounds": "[0,0,10,10]"}, "children": [ + {"attributes": {"resource-id": "HomeScreen", "bounds": "[0,0,10,5]"}, "children": []} + ]} + ]}`, + "HomeScreen", + }, + { + "cross-fade names no screen", + `{"attributes": {"bounds": "[0,0,10,10]"}, "children": [ + {"attributes": {"resource-id": "HomeScreen", "bounds": "[0,0,10,10]"}, "children": []}, + {"attributes": {"resource-id": "LedgerScreen", "bounds": "[0,0,10,10]"}, "children": []} + ]}`, + "", + }, + { + "no marker at all", + `{"attributes": {"bounds": "[0,0,10,10]"}, "children": []}`, + "", + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + tree, err := Parse(testCase.tree) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if got := tree.ScreenName(); got != testCase.want { + t.Errorf("ScreenName = %q, want %q", got, testCase.want) + } + }) + } +}