@oniroproject/core 0.12.0 → 0.13.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/README.md CHANGED
@@ -206,17 +206,23 @@ The injection-safe primitives every device operation builds on (`spawn` with `sh
206
206
  | `takeScreenshot` | `(ScreenshotOptions) => Promise<Screenshot>` | `{ pixels: Buffer, width, height }` — raw JPEG, no decode. |
207
207
  | `captureBurst` | `(CaptureBurstOptions) => Promise<Buffer[]>` | N frames `intervalMs` apart, pulled back in order. |
208
208
  | `getDisplaySize` | `(config, opts?) => Promise<DisplaySize \| null>` | Render resolution from `hidumper` (no image work). |
209
- | `dumpLayout` | `(DumpLayoutOptions) => Promise<DumpLayoutResult>` | `uitest dumpLayout` → pruned, 0–1-normalized `LayoutNode` tree. |
210
- | `parseBounds` / `pruneLayout` | — | Pure helpers behind `dumpLayout`. |
209
+ | `dumpLayout` | `(DumpLayoutOptions) => Promise<DumpLayoutResult>` | `uitest dumpLayout` → pruned, 0–1-normalized `LayoutNode` tree, with `hint`, `disabled`, and `covered` (painted over by a later filled layer). `detectKeyboard` adds `keyboard: KeyboardState`. |
210
+ | `parseBounds` / `pruneLayout` / `findCoveredNodes` | — | Pure helpers behind `dumpLayout`. |
211
+ | `flattenLayout` / `findLayoutNodes` / `pickTarget` | — | Pure: flatten a tree (nodes tagged with their `window`), match a `LayoutSelector` (`text` / `id` / `type`), and choose one uncovered, preferably clickable node (throws `TargetNotFoundError` with `reason` `none` / `covered` / `ambiguous`). |
212
+ | `getImeBundles` / `parseImeBundles` / `findKeyboardWindow` | — | Installed input methods (`hidumper -s 3703`) and the soft-keyboard window in a layout. |
211
213
 
212
214
  ### hdc — input injection
213
215
 
214
216
  | Export | Signature | Description |
215
217
  | --- | --- | --- |
216
- | `sendInput` | `(SendInputOptions) => Promise<void>` | One `uitest uiInput` action in **pixel** space (`InputType`: click/doubleClick/longClick/swipe/drag/fling/keyEvent/inputText). |
218
+ | `sendInput` | `(SendInputOptions) => Promise<void>` | One `uitest uiInput` action in **pixel** space (`InputType`: click/doubleClick/longClick/swipe/drag/fling/keyEvent/inputText). `key` accepts names and combos (see `resolveKeyEvent`). |
219
+ | `performInput` | `(PerformInputOptions) => Promise<PerformInputResult>` | `sendInput` plus a start point from an element selector (`target`), 0–1 fractions, or pixels; `clearText`, `inputText` with `clear`, and keyboard-aware `hideKeyboard`. |
220
+ | `locateTarget` | `(LocateTargetOptions) => Promise<LocatedTarget>` | Resolve a selector against the live layout to one element and its pixel center, dismissing the keyboard if it hides the element. |
221
+ | `hideKeyboard` | `(opts) => Promise<boolean>` | Press Back only when the soft keyboard is shown. |
222
+ | `waitForTarget` | `(WaitForTargetOptions) => Promise<FlatLayoutNode \| null>` | Poll the layout until an uncovered element matches (or, with `gone`, until none does). |
217
223
  | `sendGesture` | `(SendGestureOptions) => Promise<void>` | Multi-`Waypoint` path; routes through `uinput` when `holdStartMs`/`holdEndMs` need real press timing. |
218
224
  | `sendRawTouch` | `(SendRawTouchOptions) => Promise<void>` | Low-level `uinput -T` down/move/up events. |
219
- | `buildInputCommand` / `buildGestureCommands` / `buildRawTouchCommand` | — | Pure command builders (unit-testable). |
225
+ | `buildInputCommand` / `buildGestureCommands` / `buildRawTouchCommand` / `resolveKeyEvent` | — | Pure command builders (unit-testable). |
220
226
 
221
227
  ### hdc — logging (hilog)
222
228
 
package/dist/index.cjs CHANGED
@@ -47,9 +47,11 @@ __export(index_exports, {
47
47
  SIGNING_ERRORS: () => SIGNING_ERRORS,
48
48
  SdkNotInstalledError: () => SdkNotInstalledError,
49
49
  TMP_DIR_HINT: () => TMP_DIR_HINT,
50
+ TargetNotFoundError: () => TargetNotFoundError,
50
51
  UnsupportedPlatformError: () => UnsupportedPlatformError,
51
52
  VERSION: () => VERSION,
52
53
  applyChanges: () => applyChanges,
54
+ assertLayoutSelector: () => assertLayoutSelector,
53
55
  attemptHdcConnection: () => attemptHdcConnection,
54
56
  buildCmdWrapper: () => buildCmdWrapper,
55
57
  buildGestureCommands: () => buildGestureCommands,
@@ -72,6 +74,7 @@ __export(index_exports, {
72
74
  dedupEntries: () => dedupEntries,
73
75
  defaultContext: () => defaultContext,
74
76
  defaultPaths: () => defaultPaths,
77
+ describeLayoutNode: () => describeLayoutNode,
75
78
  detectAaStartFailure: () => detectAaStartFailure,
76
79
  detectProjectSdkVersion: () => detectProjectSdkVersion,
77
80
  detectRuntimeOs: () => detectRuntimeOs,
@@ -97,8 +100,12 @@ __export(index_exports, {
97
100
  extractZipWithProgress: () => extractZipWithProgress,
98
101
  findAppProcessId: () => findAppProcessId,
99
102
  findCmdToolsSourceDir: () => findCmdToolsSourceDir,
103
+ findCoveredNodes: () => findCoveredNodes,
100
104
  findHarmonyOsSdk: () => findHarmonyOsSdk,
105
+ findKeyboardWindow: () => findKeyboardWindow,
106
+ findLayoutNodes: () => findLayoutNodes,
101
107
  findRunningProcess: () => findRunningProcess,
108
+ flattenLayout: () => flattenLayout,
102
109
  forceStop: () => forceStop,
103
110
  formatBytes: () => formatBytes,
104
111
  generateSigningConfigs: () => generateSigningConfigs,
@@ -113,6 +120,7 @@ __export(index_exports, {
113
120
  getFreeSpaceBytes: () => getFreeSpaceBytes,
114
121
  getHdcPath: () => getHdcPath,
115
122
  getHvigorwPath: () => getHvigorwPath,
123
+ getImeBundles: () => getImeBundles,
116
124
  getInstalledSdks: () => getInstalledSdks,
117
125
  getKey: () => getKey,
118
126
  getMainAbility: () => getMainAbility,
@@ -125,6 +133,7 @@ __export(index_exports, {
125
133
  harmonyOsAutoSign: () => harmonyOsAutoSign,
126
134
  harmonyOsSdkLabelForApi: () => harmonyOsSdkLabelForApi,
127
135
  hdcExec: () => hdcExec,
136
+ hideKeyboard: () => hideKeyboard,
128
137
  installApp: () => installApp,
129
138
  installCmdTools: () => installCmdTools,
130
139
  installEmulator: () => installEmulator,
@@ -141,6 +150,7 @@ __export(index_exports, {
141
150
  listModules: () => listModules,
142
151
  listRunningProcesses: () => listRunningProcesses,
143
152
  listTemplates: () => listTemplates,
153
+ locateTarget: () => locateTarget,
144
154
  needsCmdWrapper: () => needsCmdWrapper,
145
155
  nonInteractivePrompter: () => nonInteractivePrompter,
146
156
  noopLogger: () => noopLogger,
@@ -151,9 +161,12 @@ __export(index_exports, {
151
161
  parseCodelinterFindings: () => parseCodelinterFindings,
152
162
  parseDeviceList: () => parseDeviceList,
153
163
  parseHilogLine: () => parseHilogLine,
164
+ parseImeBundles: () => parseImeBundles,
154
165
  parseMainAbilityFromBmDump: () => parseMainAbilityFromBmDump,
155
166
  parseRemoteExit: () => parseRemoteExit,
167
+ performInput: () => performInput,
156
168
  pickSigningKind: () => pickSigningKind,
169
+ pickTarget: () => pickTarget,
157
170
  prepareSigning: () => prepareSigning,
158
171
  pruneLayout: () => pruneLayout,
159
172
  readJson5File: () => readJson5File,
@@ -166,6 +179,7 @@ __export(index_exports, {
166
179
  removeSdk: () => removeSdk,
167
180
  removeTempWorkDir: () => removeTempWorkDir,
168
181
  requireHarmonyOsSdk: () => requireHarmonyOsSdk,
182
+ resolveKeyEvent: () => resolveKeyEvent,
169
183
  resolveTmpRoot: () => resolveTmpRoot,
170
184
  runCodelinter: () => runCodelinter,
171
185
  runHvigorw: () => runHvigorw,
@@ -195,8 +209,10 @@ __export(index_exports, {
195
209
  waitForBundle: () => waitForBundle,
196
210
  waitForCondition: () => waitForCondition,
197
211
  waitForLog: () => waitForLog,
212
+ waitForTarget: () => waitForTarget,
198
213
  watchLog: () => watchLog,
199
214
  waypointsToRawEvents: () => waypointsToRawEvents,
215
+ withoutWindow: () => withoutWindow,
200
216
  writeJson5File: () => writeJson5File,
201
217
  writeJsonFile: () => writeJsonFile
202
218
  });
@@ -2619,6 +2635,55 @@ async function captureBurst(opts) {
2619
2635
  }
2620
2636
 
2621
2637
  // src/hdc/input.ts
2638
+ var UITEST_KEY_NAMES = ["Back", "Home", "Power"];
2639
+ var KEY_IDS = {
2640
+ back: 2,
2641
+ home: 1,
2642
+ power: 18,
2643
+ volumeup: 16,
2644
+ volumedown: 17,
2645
+ up: 2012,
2646
+ down: 2013,
2647
+ left: 2014,
2648
+ right: 2015,
2649
+ alt: 2045,
2650
+ shift: 2047,
2651
+ tab: 2049,
2652
+ space: 2050,
2653
+ enter: 2054,
2654
+ delete: 2055,
2655
+ backspace: 2055,
2656
+ pageup: 2068,
2657
+ pagedown: 2069,
2658
+ escape: 2070,
2659
+ esc: 2070,
2660
+ forwarddelete: 2071,
2661
+ ctrl: 2072,
2662
+ meta: 2076,
2663
+ movehome: 2081,
2664
+ moveend: 2082
2665
+ };
2666
+ function keyId(name) {
2667
+ const k = name.trim().toLowerCase();
2668
+ if (/^\d+$/.test(k)) return Number(k);
2669
+ if (/^[a-z]$/.test(k)) return 2017 + (k.charCodeAt(0) - 97);
2670
+ const id = KEY_IDS[k];
2671
+ if (id === void 0) {
2672
+ throw new OniroError(
2673
+ `Unknown key "${name}". Use a numeric KeyCode id, a letter, or one of: ${Object.keys(KEY_IDS).join(", ")}. Combine up to three with "+", e.g. Ctrl+A.`
2674
+ );
2675
+ }
2676
+ return id;
2677
+ }
2678
+ function resolveKeyEvent(key) {
2679
+ const spec = String(key).trim();
2680
+ if (spec === "") throw new OniroError('send input "keyEvent" requires a key.');
2681
+ const native = UITEST_KEY_NAMES.find((n) => n.toLowerCase() === spec.toLowerCase());
2682
+ if (native) return native;
2683
+ const parts = spec.includes("+") ? spec.split("+") : spec.split(/\s+/);
2684
+ if (parts.length > 3) throw new OniroError(`uitest injects at most three keys at once; got "${spec}".`);
2685
+ return parts.map(keyId).join(" ");
2686
+ }
2622
2687
  function reqNum(value, name, type) {
2623
2688
  if (value === void 0 || Number.isNaN(value)) {
2624
2689
  throw new OniroError(`send input "${type}" requires a numeric ${name}.`);
@@ -2639,7 +2704,7 @@ function buildInputCommand(opts) {
2639
2704
  }
2640
2705
  case "keyEvent":
2641
2706
  if (opts.key === void 0 || opts.key === "") throw new OniroError('send input "keyEvent" requires a key.');
2642
- return `uitest uiInput keyEvent ${opts.key}`;
2707
+ return `uitest uiInput keyEvent ${resolveKeyEvent(opts.key)}`;
2643
2708
  case "inputText": {
2644
2709
  const t = String(opts.text ?? "").replace(/'/g, `'\\''`);
2645
2710
  if (opts.pxX !== void 0 && opts.pxY !== void 0) {
@@ -2737,6 +2802,48 @@ var fs20 = __toESM(require("fs"), 1);
2737
2802
  var os10 = __toESM(require("os"), 1);
2738
2803
  var path20 = __toESM(require("path"), 1);
2739
2804
  var import_node_crypto2 = require("crypto");
2805
+
2806
+ // src/hdc/ime.ts
2807
+ var INPUT_METHOD_SA_ID = 3703;
2808
+ var IME_BUNDLE_FALLBACK = /inputmethod|keyboard|(^|\.)ime(\.|$)/i;
2809
+ function parseImeBundles(output) {
2810
+ const current = [];
2811
+ const others = [];
2812
+ const entry = /\{[^{}]*"ime"\s*:\s*"([^"/]+)\/[^"]*"[^{}]*\}/g;
2813
+ for (const m of output.matchAll(entry)) {
2814
+ const bundle = m[1];
2815
+ const list = /"isCurrentIme"\s*:\s*"true"/.test(m[0]) ? current : others;
2816
+ if (!current.includes(bundle) && !others.includes(bundle)) list.push(bundle);
2817
+ }
2818
+ return [...current, ...others];
2819
+ }
2820
+ async function getImeBundles(config, opts = {}) {
2821
+ try {
2822
+ const res = await shell({
2823
+ config,
2824
+ command: `hidumper -s ${INPUT_METHOD_SA_ID} -a '-a'`,
2825
+ deviceSerial: opts.deviceSerial,
2826
+ timeoutMs: 1e4,
2827
+ logger: opts.logger
2828
+ });
2829
+ if (res.code === 0) return parseImeBundles(res.stdout);
2830
+ } catch (err) {
2831
+ opts.logger?.debug(`could not list input methods: ${err instanceof Error ? err.message : String(err)}`);
2832
+ }
2833
+ return [];
2834
+ }
2835
+ function findKeyboardWindow(tree, imeBundles) {
2836
+ const isIme = (bundle) => imeBundles.length > 0 ? imeBundles.includes(bundle) : IME_BUNDLE_FALLBACK.test(bundle);
2837
+ const stack = [tree];
2838
+ while (stack.length > 0) {
2839
+ const n = stack.shift();
2840
+ if (n.bundle && isIme(n.bundle)) return n;
2841
+ stack.push(...n.children ?? []);
2842
+ }
2843
+ return null;
2844
+ }
2845
+
2846
+ // src/hdc/dump.ts
2740
2847
  function parseBounds(s) {
2741
2848
  const m = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/.exec(s ?? "");
2742
2849
  if (!m) return null;
@@ -2771,7 +2878,46 @@ var FLAG_MAP = [
2771
2878
  ["scrollable", "scroll"],
2772
2879
  ["longClickable", "longClick"]
2773
2880
  ];
2774
- function pruneLayout(node, display) {
2881
+ function colorAlpha(color) {
2882
+ const m = /^#([0-9a-f]{2})?[0-9a-f]{6}$/i.exec(color ?? "");
2883
+ if (!m) return 0;
2884
+ return m[1] === void 0 ? 255 : parseInt(m[1], 16);
2885
+ }
2886
+ function paintsFill(a) {
2887
+ if (a.visible === "false") return false;
2888
+ if (a.opacity !== void 0 && a.opacity !== "" && !(Number(a.opacity) > 0)) return false;
2889
+ return colorAlpha(a.backgroundColor) > 0;
2890
+ }
2891
+ function findCoveredNodes(root) {
2892
+ const order = [];
2893
+ const fills = [];
2894
+ const visit = (n) => {
2895
+ const a = n.attributes ?? {};
2896
+ const entry = { node: n, bounds: parseBounds(a.bounds), end: 0 };
2897
+ if (entry.bounds && paintsFill(a)) fills.push(order.length);
2898
+ order.push(entry);
2899
+ const kids = (n.children ?? []).map((c, i) => ({ c, i, z: Number(c.attributes?.zIndex) || 0 })).sort((p, q) => p.z - q.z || p.i - q.i);
2900
+ for (const k of kids) visit(k.c);
2901
+ entry.end = order.length;
2902
+ };
2903
+ visit(root);
2904
+ const covered = /* @__PURE__ */ new Set();
2905
+ for (const p of order) {
2906
+ if (!p.bounds) continue;
2907
+ const cx = (p.bounds[0] + p.bounds[2]) / 2;
2908
+ const cy = (p.bounds[1] + p.bounds[3]) / 2;
2909
+ for (const j of fills) {
2910
+ if (j < p.end) continue;
2911
+ const q = order[j].bounds;
2912
+ if (cx >= q[0] && cx < q[2] && cy >= q[1] && cy < q[3]) {
2913
+ covered.add(p.node);
2914
+ break;
2915
+ }
2916
+ }
2917
+ }
2918
+ return covered;
2919
+ }
2920
+ function pruneLayout(node, display, covered) {
2775
2921
  const a = node.attributes ?? {};
2776
2922
  const px = parseBounds(a.bounds);
2777
2923
  const out = {};
@@ -2789,28 +2935,125 @@ function pruneLayout(node, display) {
2789
2935
  if (a.type && !STRUCTURAL_TYPES.has(a.type)) out.type = a.type;
2790
2936
  if (a.text) out.text = a.text;
2791
2937
  if (a.description) out.desc = a.description;
2938
+ if (a.hint) out.hint = a.hint;
2792
2939
  if (a.id) out.id = a.id;
2793
2940
  if (a.key && a.key !== a.id) out.key = a.key;
2794
2941
  if (a.bundleName) out.bundle = a.bundleName;
2795
2942
  for (const [attr, short] of FLAG_MAP) {
2796
2943
  if (a[attr] === "true") out[short] = true;
2797
2944
  }
2945
+ if (a.enabled === "false") out.disabled = true;
2946
+ if (covered?.has(node)) out.covered = true;
2798
2947
  const children = [];
2799
2948
  for (const c of node.children ?? []) {
2800
- const pc = pruneLayout(c, display);
2949
+ const pc = pruneLayout(c, display, covered);
2801
2950
  if (pc) {
2802
2951
  if (Array.isArray(pc)) children.push(...pc);
2803
2952
  else children.push(pc);
2804
2953
  }
2805
2954
  }
2806
2955
  const hasSignal = Boolean(
2807
- out.text || out.desc || out.id || out.key || out.click || out.scroll || out.check || out.longClick || out.bundle || out.type && out.type !== "root"
2956
+ out.text || out.desc || out.hint || out.id || out.key || out.click || out.scroll || out.check || out.longClick || out.bundle || out.type && out.type !== "root"
2808
2957
  );
2809
2958
  if (!hasSignal && children.length === 0) return null;
2810
2959
  if (!hasSignal) return children.length === 1 ? children[0] : children;
2811
2960
  if (children.length) out.children = children;
2812
2961
  return out;
2813
2962
  }
2963
+ function flattenLayout(tree) {
2964
+ const out = [];
2965
+ const visit = (n, window) => {
2966
+ const { children, ...rest } = n;
2967
+ const win = n.bundle ?? window;
2968
+ if (Object.keys(rest).length > 0) out.push({ ...rest, ...win ? { window: win } : {} });
2969
+ for (const c of children ?? []) visit(c, win);
2970
+ };
2971
+ visit(tree, void 0);
2972
+ return out;
2973
+ }
2974
+ function assertLayoutSelector(selector) {
2975
+ if (selector.text === void 0 && selector.id === void 0 && selector.type === void 0) {
2976
+ throw new OniroError("A layout selector needs a text, id, or type.");
2977
+ }
2978
+ if (selector.text !== void 0 && selector.text.trim() === "") {
2979
+ throw new OniroError("A layout selector text must not be blank.");
2980
+ }
2981
+ }
2982
+ function withoutWindow(tree, bundle) {
2983
+ const children = (tree.children ?? []).filter((c) => c.bundle !== bundle).map((c) => withoutWindow(c, bundle));
2984
+ return tree.children ? { ...tree, children } : tree;
2985
+ }
2986
+ function findLayoutNodes(tree, selector) {
2987
+ assertLayoutSelector(selector);
2988
+ const type = selector.type?.toLowerCase();
2989
+ const nodes = flattenLayout(tree).filter(
2990
+ (n) => (type === void 0 || n.type?.toLowerCase() === type) && (selector.id === void 0 || n.id === selector.id || n.key === selector.id)
2991
+ );
2992
+ if (selector.text === void 0) return nodes;
2993
+ const want = selector.text.trim().toLowerCase();
2994
+ const labels = (n) => [n.text, n.desc, n.hint].filter((s) => !!s).map((s) => s.trim().toLowerCase());
2995
+ const exact = nodes.filter((n) => labels(n).some((l) => l === want));
2996
+ if (exact.length > 0) return exact;
2997
+ return nodes.filter((n) => labels(n).some((l) => l.includes(want)));
2998
+ }
2999
+ function describeSelector(s) {
3000
+ const parts = [];
3001
+ if (s.text !== void 0) parts.push(`text "${s.text}"`);
3002
+ if (s.id !== void 0) parts.push(`id "${s.id}"`);
3003
+ if (s.type !== void 0) parts.push(`type ${s.type}`);
3004
+ return parts.join(", ");
3005
+ }
3006
+ function describeLayoutNode(n) {
3007
+ const label = n.text ?? n.desc ?? (n.hint !== void 0 ? `hint: ${n.hint}` : void 0);
3008
+ const at = n.c ? ` at ${n.c[0].toFixed(3)},${n.c[1].toFixed(3)}` : "";
3009
+ return `${n.type ?? "node"}${label !== void 0 ? ` "${label}"` : ""}${n.id ? ` #${n.id}` : ""}${at}`;
3010
+ }
3011
+ var area = (b) => b ? (b[2] - b[0]) * (b[3] - b[1]) : 0;
3012
+ function enclosesTarget(outer, inner) {
3013
+ const b = outer.b;
3014
+ const c = inner.c;
3015
+ if (!b || !c) return false;
3016
+ return c[0] >= b[0] && c[0] <= b[2] && c[1] >= b[1] && c[1] <= b[3] && area(b) > area(inner.b);
3017
+ }
3018
+ var TargetNotFoundError = class extends OniroError {
3019
+ constructor(message, reason) {
3020
+ super(message);
3021
+ this.reason = reason;
3022
+ this.name = "TargetNotFoundError";
3023
+ }
3024
+ reason;
3025
+ };
3026
+ function pickTarget(tree, selector, index) {
3027
+ const matches = findLayoutNodes(tree, selector).filter((n) => n.c !== void 0);
3028
+ const what = describeSelector(selector);
3029
+ if (matches.length === 0) {
3030
+ throw new TargetNotFoundError(`No on-screen element matches ${what}. Run \`oniro-app dump layout --flat\` to see what is there.`, "none");
3031
+ }
3032
+ const visible = matches.filter((n) => !n.covered);
3033
+ if (visible.length === 0) {
3034
+ throw new TargetNotFoundError(
3035
+ `Found ${matches.length} element(s) matching ${what}, but each is covered by an overlay (dialog, keyboard, or another layer): ${matches.map(describeLayoutNode).join("; ")}.`,
3036
+ "covered"
3037
+ );
3038
+ }
3039
+ const clickable = visible.filter((n) => n.click);
3040
+ const preferred = clickable.length > 0 ? clickable : visible;
3041
+ const candidates = preferred.filter((n) => !preferred.some((o) => o !== n && o.click && enclosesTarget(o, n)));
3042
+ if (index !== void 0) {
3043
+ const picked = candidates[index];
3044
+ if (!picked) {
3045
+ throw new TargetNotFoundError(`Index ${index} is out of range: ${candidates.length} element(s) match ${what}.`, "ambiguous");
3046
+ }
3047
+ return picked;
3048
+ }
3049
+ if (candidates.length > 1) {
3050
+ throw new TargetNotFoundError(
3051
+ `${candidates.length} elements match ${what}; pass an index to choose: ` + candidates.map((n, i) => `[${i}] ${describeLayoutNode(n)}`).join("; "),
3052
+ "ambiguous"
3053
+ );
3054
+ }
3055
+ return candidates[0];
3056
+ }
2814
3057
  async function dumpLayout(opts) {
2815
3058
  const timeoutMs = opts.timeoutMs ?? 3e4;
2816
3059
  const bundleArg = opts.bundleName ? ` -b '${opts.bundleName.replace(/'/g, `'\\''`)}'` : "";
@@ -2828,9 +3071,13 @@ ${dump.stdout}`);
2828
3071
  if (opts.prune === false) {
2829
3072
  return { display, tree: parsed };
2830
3073
  }
2831
- const prunedRaw = pruneLayout(parsed, display);
3074
+ const prunedRaw = pruneLayout(parsed, display, findCoveredNodes(parsed));
2832
3075
  const tree = Array.isArray(prunedRaw) ? { children: prunedRaw } : prunedRaw ?? { children: [] };
2833
- return { display, tree };
3076
+ if (!opts.detectKeyboard || opts.bundleName) return { display, tree };
3077
+ const imeBundles = await getImeBundles(opts.config, { deviceSerial: opts.deviceSerial, logger: opts.logger });
3078
+ const kb = findKeyboardWindow(tree, imeBundles);
3079
+ const keyboard = kb ? { shown: true, bundle: kb.bundle, ...kb.b ? { b: kb.b } : {} } : { shown: false };
3080
+ return { display, tree, keyboard };
2834
3081
  } finally {
2835
3082
  fs20.promises.unlink(local).catch(() => {
2836
3083
  });
@@ -2839,6 +3086,161 @@ ${dump.stdout}`);
2839
3086
  }
2840
3087
  }
2841
3088
 
3089
+ // src/hdc/uiActions.ts
3090
+ var FOCUS_SETTLE_MS = 400;
3091
+ var KEYBOARD_SETTLE_MS = 400;
3092
+ var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
3093
+ function targetTree(layout) {
3094
+ const kb = layout.keyboard;
3095
+ return kb?.shown && kb.bundle ? withoutWindow(layout.tree, kb.bundle) : layout.tree;
3096
+ }
3097
+ function fractionToPixel(f, size) {
3098
+ return Math.min(size - 1, Math.round(f * size));
3099
+ }
3100
+ function pressBack(opts) {
3101
+ return sendInput({ config: opts.config, type: "keyEvent", key: "Back", deviceSerial: opts.deviceSerial, logger: opts.logger });
3102
+ }
3103
+ async function hideKeyboard(opts) {
3104
+ const { keyboard } = await dumpLayout({ ...opts, detectKeyboard: true });
3105
+ if (!keyboard?.shown) return false;
3106
+ await pressBack(opts);
3107
+ await sleep(KEYBOARD_SETTLE_MS);
3108
+ return true;
3109
+ }
3110
+ async function locateTarget(opts) {
3111
+ assertLayoutSelector(opts.selector);
3112
+ const dumpOpts = { config: opts.config, detectKeyboard: true, deviceSerial: opts.deviceSerial, logger: opts.logger };
3113
+ let layout = await dumpLayout(dumpOpts);
3114
+ if (!layout.display) throw new OniroError("Could not read the display size to locate the element.");
3115
+ let keyboardHidden = false;
3116
+ let node;
3117
+ let failure;
3118
+ try {
3119
+ node = pickTarget(targetTree(layout), opts.selector, opts.index);
3120
+ } catch (err) {
3121
+ if (!(err instanceof TargetNotFoundError) || err.reason === "ambiguous") throw err;
3122
+ failure = err;
3123
+ }
3124
+ if ((!node || !node.click) && layout.keyboard?.shown && opts.hideKeyboard !== false) {
3125
+ opts.logger?.debug("target not reachable while the keyboard is up; dismissing it and looking again");
3126
+ await pressBack(opts);
3127
+ await sleep(KEYBOARD_SETTLE_MS);
3128
+ keyboardHidden = true;
3129
+ layout = await dumpLayout(dumpOpts);
3130
+ if (!layout.display) throw new OniroError("Could not read the display size to locate the element.");
3131
+ node = pickTarget(targetTree(layout), opts.selector, opts.index);
3132
+ }
3133
+ if (!node) throw failure;
3134
+ if (!node.c) throw new OniroError("The element has no on-screen position.");
3135
+ const x2 = fractionToPixel(node.c[0], layout.display.width);
3136
+ const y = fractionToPixel(node.c[1], layout.display.height);
3137
+ return { node, x: x2, y, display: layout.display, keyboardHidden };
3138
+ }
3139
+ function fraction(value, name) {
3140
+ if (value === void 0) return void 0;
3141
+ if (!(value >= 0 && value <= 1)) throw new OniroError(`${name} must be a fraction between 0 and 1; got ${value}.`);
3142
+ return value;
3143
+ }
3144
+ function pair(a, b, names) {
3145
+ if (a === void 0 && b === void 0) return void 0;
3146
+ if (a === void 0 || b === void 0) throw new OniroError(`Give both ${names}.`);
3147
+ return [a, b];
3148
+ }
3149
+ async function performInput(opts) {
3150
+ const device = { config: opts.config, deviceSerial: opts.deviceSerial, logger: opts.logger };
3151
+ const startFx = pair(fraction(opts.fx, "--fx"), fraction(opts.fy, "--fy"), "--fx and --fy");
3152
+ const startPx = pair(opts.pxX, opts.pxY, "--x and --y");
3153
+ const endFx = pair(fraction(opts.fx2, "--fx2"), fraction(opts.fy2, "--fy2"), "--fx2 and --fy2");
3154
+ const endPx = pair(opts.pxX2, opts.pxY2, "--x2 and --y2");
3155
+ if ([opts.target, startFx, startPx].filter((s) => s !== void 0).length > 1) {
3156
+ throw new OniroError("Give the start point one way: a target, fractions (--fx/--fy), or pixels (--x/--y).");
3157
+ }
3158
+ if (endFx && endPx) throw new OniroError("Give the end point one way: fractions (--fx2/--fy2) or pixels (--x2/--y2).");
3159
+ let display;
3160
+ const toPx = async ([fx, fy]) => {
3161
+ display ??= await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial, logger: opts.logger });
3162
+ if (!display) throw new OniroError("Could not read the display size to convert fractions to pixels.");
3163
+ return [fractionToPixel(fx, display.width), fractionToPixel(fy, display.height)];
3164
+ };
3165
+ const result = { keyboardHidden: false };
3166
+ let start = startPx;
3167
+ if (opts.target) {
3168
+ const located = await locateTarget({ ...device, selector: opts.target, index: opts.targetIndex });
3169
+ display = located.display;
3170
+ start = [located.x, located.y];
3171
+ result.target = located.node;
3172
+ result.keyboardHidden = located.keyboardHidden;
3173
+ } else if (startFx) {
3174
+ start = await toPx(startFx);
3175
+ }
3176
+ const end = endFx ? await toPx(endFx) : endPx;
3177
+ if (start) [result.x, result.y] = start;
3178
+ const tapToFocus = async () => {
3179
+ if (!start) return;
3180
+ await sendInput({ ...device, type: "click", pxX: start[0], pxY: start[1] });
3181
+ await sleep(FOCUS_SETTLE_MS);
3182
+ };
3183
+ const clearFocused = async () => {
3184
+ await sendInput({ ...device, type: "keyEvent", key: "Ctrl+A" });
3185
+ await sendInput({ ...device, type: "keyEvent", key: "Delete" });
3186
+ };
3187
+ switch (opts.type) {
3188
+ case "hideKeyboard":
3189
+ result.keyboardHidden = await hideKeyboard(device) || result.keyboardHidden;
3190
+ return result;
3191
+ case "clearText":
3192
+ await tapToFocus();
3193
+ await clearFocused();
3194
+ break;
3195
+ case "inputText":
3196
+ if (opts.clear) {
3197
+ await tapToFocus();
3198
+ await clearFocused();
3199
+ await sendInput({ ...device, type: "inputText", text: opts.text });
3200
+ } else {
3201
+ await sendInput({ ...device, type: "inputText", pxX: start?.[0], pxY: start?.[1], text: opts.text });
3202
+ }
3203
+ break;
3204
+ default:
3205
+ await sendInput({
3206
+ ...device,
3207
+ type: opts.type,
3208
+ pxX: start?.[0],
3209
+ pxY: start?.[1],
3210
+ pxX2: end?.[0],
3211
+ pxY2: end?.[1],
3212
+ speed: opts.speed,
3213
+ key: opts.key,
3214
+ text: opts.text
3215
+ });
3216
+ }
3217
+ if (opts.hideKeyboard) result.keyboardHidden = await hideKeyboard(device) || result.keyboardHidden;
3218
+ return result;
3219
+ }
3220
+ async function waitForTarget(opts) {
3221
+ const { selector } = opts;
3222
+ assertLayoutSelector(selector);
3223
+ let found = null;
3224
+ await waitForCondition({
3225
+ probe: async () => {
3226
+ const layout = await dumpLayout({
3227
+ config: opts.config,
3228
+ detectKeyboard: true,
3229
+ deviceSerial: opts.deviceSerial,
3230
+ logger: opts.logger
3231
+ });
3232
+ found = findLayoutNodes(targetTree(layout), selector).find((n) => !n.covered) ?? null;
3233
+ return opts.gone ? found === null : found !== null;
3234
+ },
3235
+ timeoutMs: opts.timeoutMs,
3236
+ pollMs: opts.pollMs ?? 500,
3237
+ abortSignal: opts.abortSignal,
3238
+ logger: opts.logger,
3239
+ timeoutMessage: opts.gone ? `An element matching the selector was still on screen after ${opts.timeoutMs}ms.` : `No element matching the selector appeared within ${opts.timeoutMs}ms.`
3240
+ });
3241
+ return opts.gone ? null : found;
3242
+ }
3243
+
2842
3244
  // src/hdc/hilog.ts
2843
3245
  function setHilogLevel(opts) {
2844
3246
  const logger = opts.logger ?? noopLogger;
@@ -4971,7 +5373,7 @@ async function harmonyOsAutoSign(opts) {
4971
5373
  }
4972
5374
 
4973
5375
  // src/index.ts
4974
- var VERSION = "0.12.0";
5376
+ var VERSION = "0.13.0";
4975
5377
  // Annotate the CommonJS export names for ESM import in node:
4976
5378
  0 && (module.exports = {
4977
5379
  ALL_SDKS,
@@ -4991,9 +5393,11 @@ var VERSION = "0.12.0";
4991
5393
  SIGNING_ERRORS,
4992
5394
  SdkNotInstalledError,
4993
5395
  TMP_DIR_HINT,
5396
+ TargetNotFoundError,
4994
5397
  UnsupportedPlatformError,
4995
5398
  VERSION,
4996
5399
  applyChanges,
5400
+ assertLayoutSelector,
4997
5401
  attemptHdcConnection,
4998
5402
  buildCmdWrapper,
4999
5403
  buildGestureCommands,
@@ -5016,6 +5420,7 @@ var VERSION = "0.12.0";
5016
5420
  dedupEntries,
5017
5421
  defaultContext,
5018
5422
  defaultPaths,
5423
+ describeLayoutNode,
5019
5424
  detectAaStartFailure,
5020
5425
  detectProjectSdkVersion,
5021
5426
  detectRuntimeOs,
@@ -5041,8 +5446,12 @@ var VERSION = "0.12.0";
5041
5446
  extractZipWithProgress,
5042
5447
  findAppProcessId,
5043
5448
  findCmdToolsSourceDir,
5449
+ findCoveredNodes,
5044
5450
  findHarmonyOsSdk,
5451
+ findKeyboardWindow,
5452
+ findLayoutNodes,
5045
5453
  findRunningProcess,
5454
+ flattenLayout,
5046
5455
  forceStop,
5047
5456
  formatBytes,
5048
5457
  generateSigningConfigs,
@@ -5057,6 +5466,7 @@ var VERSION = "0.12.0";
5057
5466
  getFreeSpaceBytes,
5058
5467
  getHdcPath,
5059
5468
  getHvigorwPath,
5469
+ getImeBundles,
5060
5470
  getInstalledSdks,
5061
5471
  getKey,
5062
5472
  getMainAbility,
@@ -5069,6 +5479,7 @@ var VERSION = "0.12.0";
5069
5479
  harmonyOsAutoSign,
5070
5480
  harmonyOsSdkLabelForApi,
5071
5481
  hdcExec,
5482
+ hideKeyboard,
5072
5483
  installApp,
5073
5484
  installCmdTools,
5074
5485
  installEmulator,
@@ -5085,6 +5496,7 @@ var VERSION = "0.12.0";
5085
5496
  listModules,
5086
5497
  listRunningProcesses,
5087
5498
  listTemplates,
5499
+ locateTarget,
5088
5500
  needsCmdWrapper,
5089
5501
  nonInteractivePrompter,
5090
5502
  noopLogger,
@@ -5095,9 +5507,12 @@ var VERSION = "0.12.0";
5095
5507
  parseCodelinterFindings,
5096
5508
  parseDeviceList,
5097
5509
  parseHilogLine,
5510
+ parseImeBundles,
5098
5511
  parseMainAbilityFromBmDump,
5099
5512
  parseRemoteExit,
5513
+ performInput,
5100
5514
  pickSigningKind,
5515
+ pickTarget,
5101
5516
  prepareSigning,
5102
5517
  pruneLayout,
5103
5518
  readJson5File,
@@ -5110,6 +5525,7 @@ var VERSION = "0.12.0";
5110
5525
  removeSdk,
5111
5526
  removeTempWorkDir,
5112
5527
  requireHarmonyOsSdk,
5528
+ resolveKeyEvent,
5113
5529
  resolveTmpRoot,
5114
5530
  runCodelinter,
5115
5531
  runHvigorw,
@@ -5139,8 +5555,10 @@ var VERSION = "0.12.0";
5139
5555
  waitForBundle,
5140
5556
  waitForCondition,
5141
5557
  waitForLog,
5558
+ waitForTarget,
5142
5559
  watchLog,
5143
5560
  waypointsToRawEvents,
5561
+ withoutWindow,
5144
5562
  writeJson5File,
5145
5563
  writeJsonFile
5146
5564
  });