@sensaiorg/adapter-android 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android-adapter.d.ts.map +1 -0
- package/dist/android-adapter.js +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/accessibility.d.ts.map +1 -0
- package/dist/tools/accessibility.js +85 -0
- package/dist/tools/adb.d.ts.map +1 -0
- package/dist/tools/adb.js +66 -0
- package/dist/tools/app-state.d.ts.map +1 -0
- package/dist/tools/app-state.js +173 -0
- package/dist/tools/diagnose.d.ts.map +1 -0
- package/dist/tools/diagnose.js +128 -0
- package/dist/tools/hot-reload.d.ts.map +1 -0
- package/dist/tools/hot-reload.js +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/interaction.d.ts.map +1 -0
- package/dist/tools/interaction.js +395 -0
- package/dist/tools/logcat.d.ts.map +1 -0
- package/dist/tools/logcat.js +216 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +123 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +143 -0
- package/dist/tools/recording.d.ts.map +1 -0
- package/dist/tools/recording.js +102 -0
- package/dist/tools/rn-tools.d.ts.map +1 -0
- package/dist/tools/rn-tools.js +120 -0
- package/dist/tools/smart-actions.d.ts.map +1 -0
- package/dist/tools/smart-actions.js +506 -0
- package/dist/tools/ui-tree.d.ts.map +1 -0
- package/dist/tools/ui-tree.js +226 -0
- package/dist/transport/adb-client.d.ts.map +1 -0
- package/dist/transport/adb-client.js +124 -0
- package/dist/transport/adb-client.test.d.ts.map +1 -0
- package/dist/transport/adb-client.test.js +153 -0
- package/dist/transport/agent-client.d.ts.map +1 -0
- package/dist/transport/agent-client.js +157 -0
- package/dist/transport/agent-client.test.d.ts.map +1 -0
- package/dist/transport/agent-client.test.js +199 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +119 -0
- package/dist/util/logcat-parser.d.ts.map +1 -0
- package/dist/util/logcat-parser.js +79 -0
- package/dist/util/safety.d.ts.map +1 -0
- package/dist/util/safety.js +132 -0
- package/dist/util/safety.test.d.ts.map +1 -0
- package/dist/util/safety.test.js +205 -0
- package/dist/util/text-extractor.d.ts.map +1 -0
- package/dist/util/text-extractor.js +71 -0
- package/dist/util/ui-tree-cache.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.js +46 -0
- package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.test.js +84 -0
- package/dist/util/ui-tree-parser.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.js +123 -0
- package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.test.js +167 -0
- package/package.json +22 -0
- package/src/android-adapter.ts +124 -0
- package/src/index.ts +8 -0
- package/src/tools/accessibility.ts +94 -0
- package/src/tools/adb.ts +75 -0
- package/src/tools/app-state.ts +193 -0
- package/src/tools/diagnose.ts +146 -0
- package/src/tools/hot-reload.ts +103 -0
- package/src/tools/index.ts +66 -0
- package/src/tools/interaction.ts +448 -0
- package/src/tools/logcat.ts +252 -0
- package/src/tools/network.ts +145 -0
- package/src/tools/performance.ts +169 -0
- package/src/tools/recording.ts +123 -0
- package/src/tools/rn-tools.ts +143 -0
- package/src/tools/smart-actions.ts +593 -0
- package/src/tools/ui-tree.ts +258 -0
- package/src/transport/adb-client.test.ts +228 -0
- package/src/transport/adb-client.ts +139 -0
- package/src/transport/agent-client.test.ts +267 -0
- package/src/transport/agent-client.ts +188 -0
- package/src/transport/connection-manager.ts +140 -0
- package/src/util/logcat-parser.ts +94 -0
- package/src/util/safety.test.ts +251 -0
- package/src/util/safety.ts +143 -0
- package/src/util/text-extractor.ts +87 -0
- package/src/util/ui-tree-cache.test.ts +105 -0
- package/src/util/ui-tree-cache.ts +54 -0
- package/src/util/ui-tree-parser.test.ts +182 -0
- package/src/util/ui-tree-parser.ts +169 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseUiTree, flattenTree } from "./ui-tree-parser.js";
|
|
3
|
+
|
|
4
|
+
const MINIMAL_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<hierarchy rotation="0">
|
|
6
|
+
<node index="0" text="Hello" resource-id="com.example:id/greeting"
|
|
7
|
+
class="android.widget.TextView" package="com.example"
|
|
8
|
+
content-desc="Greeting text" checkable="false" checked="false"
|
|
9
|
+
clickable="true" enabled="true" focusable="true" focused="false"
|
|
10
|
+
scrollable="false" long-clickable="false" password="false"
|
|
11
|
+
selected="false" bounds="[0,0][500,100]">
|
|
12
|
+
</node>
|
|
13
|
+
</hierarchy>`;
|
|
14
|
+
|
|
15
|
+
const NESTED_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
16
|
+
<hierarchy rotation="0">
|
|
17
|
+
<node index="0" text="" resource-id="" class="android.widget.FrameLayout"
|
|
18
|
+
package="com.example" content-desc="" checkable="false" checked="false"
|
|
19
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
20
|
+
scrollable="false" long-clickable="false" password="false"
|
|
21
|
+
selected="false" bounds="[0,0][1080,1920]">
|
|
22
|
+
<node index="0" text="Button 1" resource-id="com.example:id/btn1"
|
|
23
|
+
class="android.widget.Button" package="com.example"
|
|
24
|
+
content-desc="First button" checkable="false" checked="false"
|
|
25
|
+
clickable="true" enabled="true" focusable="true" focused="false"
|
|
26
|
+
scrollable="false" long-clickable="false" password="false"
|
|
27
|
+
selected="false" bounds="[50,100][300,200]">
|
|
28
|
+
</node>
|
|
29
|
+
<node index="1" text="Button 2" resource-id="com.example:id/btn2"
|
|
30
|
+
class="android.widget.Button" package="com.example"
|
|
31
|
+
content-desc="" checkable="false" checked="false"
|
|
32
|
+
clickable="true" enabled="true" focusable="true" focused="false"
|
|
33
|
+
scrollable="false" long-clickable="false" password="false"
|
|
34
|
+
selected="false" bounds="[50,250][300,350]">
|
|
35
|
+
</node>
|
|
36
|
+
</node>
|
|
37
|
+
</hierarchy>`;
|
|
38
|
+
|
|
39
|
+
const SYSTEM_UI_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<hierarchy rotation="0">
|
|
41
|
+
<node index="0" text="12:00" resource-id="" class="android.widget.TextView"
|
|
42
|
+
package="com.android.systemui" content-desc="" checkable="false" checked="false"
|
|
43
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
44
|
+
scrollable="false" long-clickable="false" password="false"
|
|
45
|
+
selected="false" bounds="[0,0][100,50]">
|
|
46
|
+
</node>
|
|
47
|
+
<node index="1" text="App Content" resource-id="" class="android.widget.TextView"
|
|
48
|
+
package="com.example" content-desc="" checkable="false" checked="false"
|
|
49
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
50
|
+
scrollable="false" long-clickable="false" password="false"
|
|
51
|
+
selected="false" bounds="[0,50][1080,1920]">
|
|
52
|
+
</node>
|
|
53
|
+
</hierarchy>`;
|
|
54
|
+
|
|
55
|
+
describe("parseUiTree", () => {
|
|
56
|
+
it("parses minimal XML with one node", () => {
|
|
57
|
+
const nodes = parseUiTree(MINIMAL_XML);
|
|
58
|
+
expect(nodes).toHaveLength(1);
|
|
59
|
+
|
|
60
|
+
const node = nodes[0];
|
|
61
|
+
expect(node.text).toBe("Hello");
|
|
62
|
+
expect(node.resourceId).toBe("com.example:id/greeting");
|
|
63
|
+
expect(node.className).toBe("android.widget.TextView");
|
|
64
|
+
expect(node.contentDescription).toBe("Greeting text");
|
|
65
|
+
expect(node.clickable).toBe(true);
|
|
66
|
+
expect(node.enabled).toBe(true);
|
|
67
|
+
expect(node.focusable).toBe(true);
|
|
68
|
+
expect(node.scrollable).toBe(false);
|
|
69
|
+
expect(node.checked).toBe(false);
|
|
70
|
+
expect(node.selected).toBe(false);
|
|
71
|
+
expect(node.packageName).toBe("com.example");
|
|
72
|
+
expect(node.depth).toBe(0);
|
|
73
|
+
expect(node.bounds).toEqual({ left: 0, top: 0, right: 500, bottom: 100 });
|
|
74
|
+
expect(node.children).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("parses nested nodes", () => {
|
|
78
|
+
const nodes = parseUiTree(NESTED_XML);
|
|
79
|
+
expect(nodes).toHaveLength(1);
|
|
80
|
+
expect(nodes[0].children).toHaveLength(2);
|
|
81
|
+
expect(nodes[0].children[0].text).toBe("Button 1");
|
|
82
|
+
expect(nodes[0].children[1].text).toBe("Button 2");
|
|
83
|
+
expect(nodes[0].children[0].depth).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("filters system UI by default", () => {
|
|
87
|
+
const nodes = parseUiTree(SYSTEM_UI_XML);
|
|
88
|
+
expect(nodes).toHaveLength(1);
|
|
89
|
+
expect(nodes[0].text).toBe("App Content");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("includes system UI when requested", () => {
|
|
93
|
+
const nodes = parseUiTree(SYSTEM_UI_XML, { includeSystemUI: true });
|
|
94
|
+
expect(nodes).toHaveLength(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("respects maxDepth", () => {
|
|
98
|
+
const nodes = parseUiTree(NESTED_XML, { maxDepth: 1 });
|
|
99
|
+
expect(nodes).toHaveLength(1);
|
|
100
|
+
// maxDepth=1 means depth 0 is included, children at depth 1 are included but not their children
|
|
101
|
+
expect(nodes[0].children).toHaveLength(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("maxDepth=0 means unlimited", () => {
|
|
105
|
+
const nodes = parseUiTree(NESTED_XML, { maxDepth: 0 });
|
|
106
|
+
expect(nodes[0].children).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns empty array for empty hierarchy", () => {
|
|
110
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?><hierarchy rotation="0"></hierarchy>`;
|
|
111
|
+
expect(parseUiTree(xml)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns empty array for missing hierarchy", () => {
|
|
115
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?><root></root>`;
|
|
116
|
+
expect(parseUiTree(xml)).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns empty array for non-XML input", () => {
|
|
120
|
+
// fast-xml-parser is lenient — doesn't throw on all malformed input
|
|
121
|
+
const nodes = parseUiTree("not xml at all");
|
|
122
|
+
expect(nodes).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("handles node with no bounds", () => {
|
|
126
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
127
|
+
<hierarchy rotation="0">
|
|
128
|
+
<node index="0" text="NoBounds" resource-id="" class="android.view.View"
|
|
129
|
+
package="com.example" content-desc="" checkable="false" checked="false"
|
|
130
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
131
|
+
scrollable="false" long-clickable="false" password="false"
|
|
132
|
+
selected="false" bounds="">
|
|
133
|
+
</node>
|
|
134
|
+
</hierarchy>`;
|
|
135
|
+
const nodes = parseUiTree(xml);
|
|
136
|
+
expect(nodes).toHaveLength(1);
|
|
137
|
+
expect(nodes[0].bounds).toBeNull();
|
|
138
|
+
expect(nodes[0].visible).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("filters invisible nodes with visibleOnly", () => {
|
|
142
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
143
|
+
<hierarchy rotation="0">
|
|
144
|
+
<node index="0" text="Visible" resource-id="" class="android.view.View"
|
|
145
|
+
package="com.example" content-desc="" checkable="false" checked="false"
|
|
146
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
147
|
+
scrollable="false" long-clickable="false" password="false"
|
|
148
|
+
selected="false" bounds="[0,0][100,100]">
|
|
149
|
+
</node>
|
|
150
|
+
<node index="1" text="Invisible" resource-id="" class="android.view.View"
|
|
151
|
+
package="com.example" content-desc="" checkable="false" checked="false"
|
|
152
|
+
clickable="false" enabled="true" focusable="false" focused="false"
|
|
153
|
+
scrollable="false" long-clickable="false" password="false"
|
|
154
|
+
selected="false" bounds="">
|
|
155
|
+
</node>
|
|
156
|
+
</hierarchy>`;
|
|
157
|
+
const nodes = parseUiTree(xml, { visibleOnly: true });
|
|
158
|
+
expect(nodes).toHaveLength(1);
|
|
159
|
+
expect(nodes[0].text).toBe("Visible");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("flattenTree", () => {
|
|
164
|
+
it("flattens nested tree to array", () => {
|
|
165
|
+
const nodes = parseUiTree(NESTED_XML);
|
|
166
|
+
const flat = flattenTree(nodes);
|
|
167
|
+
expect(flat).toHaveLength(3); // root + 2 children
|
|
168
|
+
expect(flat[0].className).toBe("android.widget.FrameLayout");
|
|
169
|
+
expect(flat[1].text).toBe("Button 1");
|
|
170
|
+
expect(flat[2].text).toBe("Button 2");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("handles empty array", () => {
|
|
174
|
+
expect(flattenTree([])).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles single node", () => {
|
|
178
|
+
const nodes = parseUiTree(MINIMAL_XML);
|
|
179
|
+
const flat = flattenTree(nodes);
|
|
180
|
+
expect(flat).toHaveLength(1);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Tree Parser - Converts Android uiautomator XML dump into structured JSON.
|
|
3
|
+
*
|
|
4
|
+
* The XML comes from `adb shell uiautomator dump` and represents the full
|
|
5
|
+
* view hierarchy of the screen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { XMLParser } from "fast-xml-parser";
|
|
9
|
+
|
|
10
|
+
/** A single node in the parsed UI tree. */
|
|
11
|
+
export interface UiNode {
|
|
12
|
+
className: string;
|
|
13
|
+
resourceId: string;
|
|
14
|
+
text: string;
|
|
15
|
+
contentDescription: string;
|
|
16
|
+
bounds: { left: number; top: number; right: number; bottom: number } | null;
|
|
17
|
+
clickable: boolean;
|
|
18
|
+
focusable: boolean;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
visible: boolean;
|
|
21
|
+
scrollable: boolean;
|
|
22
|
+
checked: boolean;
|
|
23
|
+
selected: boolean;
|
|
24
|
+
packageName: string;
|
|
25
|
+
depth: number;
|
|
26
|
+
children: UiNode[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Options for controlling the parse output. */
|
|
30
|
+
export interface ParseOptions {
|
|
31
|
+
/** Maximum depth to traverse (0 = unlimited). */
|
|
32
|
+
maxDepth?: number;
|
|
33
|
+
/** Only include nodes that are on-screen. */
|
|
34
|
+
visibleOnly?: boolean;
|
|
35
|
+
/** Include system UI nodes (status bar, nav bar). */
|
|
36
|
+
includeSystemUI?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** System UI packages to filter out. */
|
|
40
|
+
const SYSTEM_UI_PACKAGES = new Set([
|
|
41
|
+
"com.android.systemui",
|
|
42
|
+
"com.android.launcher",
|
|
43
|
+
"com.android.launcher3",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a uiautomator XML dump into a structured UiNode tree.
|
|
48
|
+
*/
|
|
49
|
+
export function parseUiTree(xml: string, options: ParseOptions = {}): UiNode[] {
|
|
50
|
+
const { maxDepth = 0, visibleOnly = false, includeSystemUI = false } = options;
|
|
51
|
+
|
|
52
|
+
const parser = new XMLParser({
|
|
53
|
+
ignoreAttributes: false,
|
|
54
|
+
attributeNamePrefix: "@_",
|
|
55
|
+
isArray: (name) => name === "node",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let parsed: Record<string, unknown>;
|
|
59
|
+
try {
|
|
60
|
+
parsed = parser.parse(xml) as Record<string, unknown>;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw new Error(`Failed to parse UI tree XML: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hierarchy = parsed["hierarchy"] as Record<string, unknown> | undefined;
|
|
66
|
+
if (!hierarchy) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rootNodes = hierarchy["node"];
|
|
71
|
+
if (!rootNodes) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nodes = Array.isArray(rootNodes) ? rootNodes : [rootNodes];
|
|
76
|
+
return nodes
|
|
77
|
+
.map((n) => convertNode(n as Record<string, unknown>, 0, maxDepth, visibleOnly, includeSystemUI))
|
|
78
|
+
.filter((n): n is UiNode => n !== null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Recursively convert a raw XML node object into a UiNode.
|
|
83
|
+
*/
|
|
84
|
+
function convertNode(
|
|
85
|
+
raw: Record<string, unknown>,
|
|
86
|
+
depth: number,
|
|
87
|
+
maxDepth: number,
|
|
88
|
+
visibleOnly: boolean,
|
|
89
|
+
includeSystemUI: boolean,
|
|
90
|
+
): UiNode | null {
|
|
91
|
+
const attr = (key: string): string => String(raw[`@_${key}`] ?? "");
|
|
92
|
+
const boolAttr = (key: string): boolean => attr(key) === "true";
|
|
93
|
+
|
|
94
|
+
const packageName = attr("package");
|
|
95
|
+
|
|
96
|
+
// Filter system UI
|
|
97
|
+
if (!includeSystemUI && SYSTEM_UI_PACKAGES.has(packageName)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const bounds = parseBounds(attr("bounds"));
|
|
102
|
+
const visible = bounds !== null && bounds.right > bounds.left && bounds.bottom > bounds.top;
|
|
103
|
+
|
|
104
|
+
if (visibleOnly && !visible) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Recurse into children (respecting maxDepth)
|
|
109
|
+
let children: UiNode[] = [];
|
|
110
|
+
if (maxDepth === 0 || depth < maxDepth) {
|
|
111
|
+
const rawChildren = raw["node"];
|
|
112
|
+
if (rawChildren) {
|
|
113
|
+
const childArray = Array.isArray(rawChildren) ? rawChildren : [rawChildren];
|
|
114
|
+
children = childArray
|
|
115
|
+
.map((c) => convertNode(c as Record<string, unknown>, depth + 1, maxDepth, visibleOnly, includeSystemUI))
|
|
116
|
+
.filter((c): c is UiNode => c !== null);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
className: attr("class"),
|
|
122
|
+
resourceId: attr("resource-id"),
|
|
123
|
+
text: attr("text"),
|
|
124
|
+
contentDescription: attr("content-desc"),
|
|
125
|
+
bounds,
|
|
126
|
+
clickable: boolAttr("clickable"),
|
|
127
|
+
focusable: boolAttr("focusable"),
|
|
128
|
+
enabled: boolAttr("enabled"),
|
|
129
|
+
visible,
|
|
130
|
+
scrollable: boolAttr("scrollable"),
|
|
131
|
+
checked: boolAttr("checked"),
|
|
132
|
+
selected: boolAttr("selected"),
|
|
133
|
+
packageName,
|
|
134
|
+
depth,
|
|
135
|
+
children,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse Android bounds string "[left,top][right,bottom]" into an object.
|
|
141
|
+
*/
|
|
142
|
+
function parseBounds(boundsStr: string): UiNode["bounds"] {
|
|
143
|
+
const match = boundsStr.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
144
|
+
if (!match) return null;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
left: parseInt(match[1], 10),
|
|
148
|
+
top: parseInt(match[2], 10),
|
|
149
|
+
right: parseInt(match[3], 10),
|
|
150
|
+
bottom: parseInt(match[4], 10),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Flatten the UI tree into an array of nodes (pre-order traversal).
|
|
156
|
+
*/
|
|
157
|
+
export function flattenTree(nodes: UiNode[]): UiNode[] {
|
|
158
|
+
const result: UiNode[] = [];
|
|
159
|
+
const stack = [...nodes];
|
|
160
|
+
while (stack.length > 0) {
|
|
161
|
+
const node = stack.pop()!;
|
|
162
|
+
result.push(node);
|
|
163
|
+
// Push children in reverse so first child is processed first
|
|
164
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
165
|
+
stack.push(node.children[i]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|