@qontinui/ui-bridge-native 0.6.6 → 0.6.8

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.
@@ -2021,7 +2021,7 @@ function diagnosePageHealth(elements, viewport) {
2021
2021
  let outsideViewport = 0;
2022
2022
  for (const { rect } of visible) {
2023
2023
  if (rect.width === 0 || rect.height === 0) zeroSize++;
2024
- if (rect.x + rect.width < 0 || rect.y + rect.height < 0 || rect.x > 1 || rect.y > 1) {
2024
+ if (rect.x + rect.width < 0 || rect.x > 1) {
2025
2025
  outsideViewport++;
2026
2026
  }
2027
2027
  }
@@ -2029,7 +2029,7 @@ function diagnosePageHealth(elements, viewport) {
2029
2029
  findings.push({
2030
2030
  check: "visual_anomalies",
2031
2031
  severity: anomalySeverity,
2032
- detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport}`,
2032
+ detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport} (horizontal-overflow only; below/above-fold scrollable content is not an anomaly)`,
2033
2033
  data: { zero_size: zeroSize, outside_viewport: outsideViewport }
2034
2034
  });
2035
2035
  const heatmap = grid.map(
@@ -2045,6 +2045,35 @@ function diagnosePageHealth(elements, viewport) {
2045
2045
  }
2046
2046
 
2047
2047
  // src/server/handlers.ts
2048
+ function matchElementsByText(registry, text, opts) {
2049
+ const needle = text.toLowerCase();
2050
+ const contains = (v) => typeof v === "string" && v.toLowerCase().includes(needle);
2051
+ let all = registry.getAllElements();
2052
+ if (opts?.visibleOnly) {
2053
+ all = all.filter((e) => e.getState().visible);
2054
+ }
2055
+ const scored = [];
2056
+ for (const e of all) {
2057
+ let rank = Number.POSITIVE_INFINITY;
2058
+ if (contains(e.label)) rank = Math.min(rank, 0);
2059
+ const ident = e.getIdentifier();
2060
+ if (ident) {
2061
+ if (contains(ident.testId)) rank = Math.min(rank, 1);
2062
+ if (contains(ident.accessibilityLabel)) rank = Math.min(rank, 1);
2063
+ if (contains(ident.treePath)) rank = Math.min(rank, 1);
2064
+ if (contains(ident.uiId)) rank = Math.min(rank, 1);
2065
+ }
2066
+ if (rank === Number.POSITIVE_INFINITY) {
2067
+ const handlers = extractHandlerNames(e.props);
2068
+ if (handlers.some((h) => h.toLowerCase().includes(needle))) rank = 2;
2069
+ }
2070
+ if (rank !== Number.POSITIVE_INFINITY) {
2071
+ scored.push({ rank, element: { id: e.id, type: e.type, label: e.label } });
2072
+ }
2073
+ }
2074
+ scored.sort((a, b) => a.rank - b.rank);
2075
+ return scored.map((s) => s.element);
2076
+ }
2048
2077
  async function executeWorkflowStep(step, registry, executor) {
2049
2078
  const startTime = Date.now();
2050
2079
  try {
@@ -2641,6 +2670,89 @@ function createServerHandlers(registry, executor, config) {
2641
2670
  const failedCount = results.length - succeededCount;
2642
2671
  return success2({ results, succeededCount, failedCount });
2643
2672
  },
2673
+ // ========================================================================
2674
+ // App-agnostic interaction parity (cross-platform `/control/page/*`)
2675
+ //
2676
+ // Mirrors the web/runner convenience endpoints so the SAME contract drives
2677
+ // mobile. Text/label interactions compose `executor.find` (registry-based
2678
+ // discovery) + `executor.executeAction` (press/type). Selector-only
2679
+ // variants have no React-Native analog and return NOT_SUPPORTED — the
2680
+ // mobile bridge's snapshot + `/control/find` (testID / accessibilityLabel)
2681
+ // cover what a selector would on web.
2682
+ // ========================================================================
2683
+ clickByText: async (ctx) => {
2684
+ const body = ctx.body;
2685
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2686
+ if (!text) {
2687
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2688
+ }
2689
+ const match = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly })[0];
2690
+ if (!match) {
2691
+ return error2(`No element found with text "${text}"`, "ELEMENT_NOT_FOUND");
2692
+ }
2693
+ const response = await executor.executeAction(match.id, { action: "press" });
2694
+ if (!response.success) {
2695
+ return error2(response.error || "Press failed", "ACTION_FAILED");
2696
+ }
2697
+ return success2({
2698
+ clicked: true,
2699
+ element: { id: match.id, type: match.type, label: match.label }
2700
+ });
2701
+ },
2702
+ clickBySelector: async () => {
2703
+ return error2(
2704
+ "clickBySelector is not supported on native platform \u2014 React Native has no CSS-selector DOM. Use clickByText, or /control/find with testIdPattern/accessibilityLabelPattern then /control/element/:id/action.",
2705
+ "NOT_SUPPORTED"
2706
+ );
2707
+ },
2708
+ typeInto: async (ctx) => {
2709
+ const body = ctx.body;
2710
+ const label = typeof body?.label === "string" ? body.label.trim() : "";
2711
+ if (!label) {
2712
+ return error2(
2713
+ "label is required on native (selector-based typeInto is web-only)",
2714
+ "INVALID_REQUEST"
2715
+ );
2716
+ }
2717
+ const text = typeof body?.text === "string" ? body.text : "";
2718
+ const match = matchElementsByText(registry, label, { visibleOnly: body?.visibleOnly })[0];
2719
+ if (!match) {
2720
+ return error2(`No input found for label "${label}"`, "ELEMENT_NOT_FOUND");
2721
+ }
2722
+ const response = await executor.executeAction(match.id, {
2723
+ action: "type",
2724
+ params: { text, clear: body?.clear === true }
2725
+ });
2726
+ if (!response.success) {
2727
+ return error2(response.error || "Type failed", "ACTION_FAILED");
2728
+ }
2729
+ return success2({
2730
+ typed: true,
2731
+ element: { id: match.id, type: match.type, label: match.label }
2732
+ });
2733
+ },
2734
+ readValue: async () => {
2735
+ return error2(
2736
+ "readValue is not supported on native platform \u2014 React Native has no CSS-selector DOM. Use /control/element/:id/state to read an element value.",
2737
+ "NOT_SUPPORTED"
2738
+ );
2739
+ },
2740
+ findByText: async (ctx) => {
2741
+ const body = ctx.body;
2742
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
2743
+ if (!text) {
2744
+ return error2("text is required and must not be empty", "INVALID_REQUEST");
2745
+ }
2746
+ const matches = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly });
2747
+ return success2(
2748
+ matches.map((el, index) => ({
2749
+ index,
2750
+ id: el.id,
2751
+ type: el.type,
2752
+ label: el.label
2753
+ }))
2754
+ );
2755
+ },
2644
2756
  // Coord-based tap
2645
2757
  //
2646
2758
  // Synthesizes a press at the given screen coordinates by scanning the
@@ -3247,6 +3359,17 @@ var WS_ROUTES = [
3247
3359
  // Coord-based tap — synthesizes a press at the given screen coords by
3248
3360
  // matching against registered elements' layout rects.
3249
3361
  { pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
3362
+ // App-agnostic interaction parity — mirrors the web/runner convenience
3363
+ // endpoints (`/control/page/click-by-text` etc.) so the SAME cross-platform
3364
+ // contract drives mobile. Text/label-based interactions compose
3365
+ // `executor.find` + `executor.executeAction` over the registry; the
3366
+ // DOM-selector-only variants (`click-by-selector`, `read-value`) have no RN
3367
+ // analog and return a structured NOT_SUPPORTED envelope.
3368
+ { pattern: "control/page/click-by-text", handler: "clickByText", httpMethods: ["POST"] },
3369
+ { pattern: "control/page/click-by-selector", handler: "clickBySelector", httpMethods: ["POST"] },
3370
+ { pattern: "control/page/type-into", handler: "typeInto", httpMethods: ["POST"] },
3371
+ { pattern: "control/page/read-value", handler: "readValue", httpMethods: ["POST"] },
3372
+ { pattern: "control/page/find-by-text", handler: "findByText", httpMethods: ["POST"] },
3250
3373
  // Test hooks — gated behind `features.testHooks`. Let external runners
3251
3374
  // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
3252
3375
  // without mounting React components.
@@ -3322,6 +3445,11 @@ var HANDLER_DESCRIPTIONS = {
3322
3445
  aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
3323
3446
  aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
3324
3447
  tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
3448
+ clickByText: "App-agnostic: press the first element matching free-text (label/accessibilityLabel/testID): body { text, visibleOnly? }",
3449
+ clickBySelector: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use clickByText or /control/find + /control/element/:id/action.",
3450
+ typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
3451
+ readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
3452
+ findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
3325
3453
  getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
3326
3454
  getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
3327
3455
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",