@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.
- package/dist/index.js +130 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +130 -2
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +130 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +130 -2
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +130 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +130 -2
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/page-health.d.ts +12 -0
- package/dist/server/page-health.d.ts.map +1 -1
- package/dist/server/types.d.ts +16 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2071,7 +2071,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2071
2071
|
let outsideViewport = 0;
|
|
2072
2072
|
for (const { rect } of visible) {
|
|
2073
2073
|
if (rect.width === 0 || rect.height === 0) zeroSize++;
|
|
2074
|
-
if (rect.x + rect.width < 0 || rect.
|
|
2074
|
+
if (rect.x + rect.width < 0 || rect.x > 1) {
|
|
2075
2075
|
outsideViewport++;
|
|
2076
2076
|
}
|
|
2077
2077
|
}
|
|
@@ -2079,7 +2079,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2079
2079
|
findings.push({
|
|
2080
2080
|
check: "visual_anomalies",
|
|
2081
2081
|
severity: anomalySeverity,
|
|
2082
|
-
detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport}`,
|
|
2082
|
+
detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport} (horizontal-overflow only; below/above-fold scrollable content is not an anomaly)`,
|
|
2083
2083
|
data: { zero_size: zeroSize, outside_viewport: outsideViewport }
|
|
2084
2084
|
});
|
|
2085
2085
|
const heatmap = grid.map(
|
|
@@ -2095,6 +2095,35 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2095
2095
|
}
|
|
2096
2096
|
|
|
2097
2097
|
// src/server/handlers.ts
|
|
2098
|
+
function matchElementsByText(registry, text, opts) {
|
|
2099
|
+
const needle = text.toLowerCase();
|
|
2100
|
+
const contains = (v) => typeof v === "string" && v.toLowerCase().includes(needle);
|
|
2101
|
+
let all = registry.getAllElements();
|
|
2102
|
+
if (opts?.visibleOnly) {
|
|
2103
|
+
all = all.filter((e) => e.getState().visible);
|
|
2104
|
+
}
|
|
2105
|
+
const scored = [];
|
|
2106
|
+
for (const e of all) {
|
|
2107
|
+
let rank = Number.POSITIVE_INFINITY;
|
|
2108
|
+
if (contains(e.label)) rank = Math.min(rank, 0);
|
|
2109
|
+
const ident = e.getIdentifier();
|
|
2110
|
+
if (ident) {
|
|
2111
|
+
if (contains(ident.testId)) rank = Math.min(rank, 1);
|
|
2112
|
+
if (contains(ident.accessibilityLabel)) rank = Math.min(rank, 1);
|
|
2113
|
+
if (contains(ident.treePath)) rank = Math.min(rank, 1);
|
|
2114
|
+
if (contains(ident.uiId)) rank = Math.min(rank, 1);
|
|
2115
|
+
}
|
|
2116
|
+
if (rank === Number.POSITIVE_INFINITY) {
|
|
2117
|
+
const handlers = extractHandlerNames(e.props);
|
|
2118
|
+
if (handlers.some((h) => h.toLowerCase().includes(needle))) rank = 2;
|
|
2119
|
+
}
|
|
2120
|
+
if (rank !== Number.POSITIVE_INFINITY) {
|
|
2121
|
+
scored.push({ rank, element: { id: e.id, type: e.type, label: e.label } });
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
scored.sort((a, b) => a.rank - b.rank);
|
|
2125
|
+
return scored.map((s) => s.element);
|
|
2126
|
+
}
|
|
2098
2127
|
async function executeWorkflowStep(step, registry, executor) {
|
|
2099
2128
|
const startTime = Date.now();
|
|
2100
2129
|
try {
|
|
@@ -2691,6 +2720,89 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2691
2720
|
const failedCount = results.length - succeededCount;
|
|
2692
2721
|
return success2({ results, succeededCount, failedCount });
|
|
2693
2722
|
},
|
|
2723
|
+
// ========================================================================
|
|
2724
|
+
// App-agnostic interaction parity (cross-platform `/control/page/*`)
|
|
2725
|
+
//
|
|
2726
|
+
// Mirrors the web/runner convenience endpoints so the SAME contract drives
|
|
2727
|
+
// mobile. Text/label interactions compose `executor.find` (registry-based
|
|
2728
|
+
// discovery) + `executor.executeAction` (press/type). Selector-only
|
|
2729
|
+
// variants have no React-Native analog and return NOT_SUPPORTED — the
|
|
2730
|
+
// mobile bridge's snapshot + `/control/find` (testID / accessibilityLabel)
|
|
2731
|
+
// cover what a selector would on web.
|
|
2732
|
+
// ========================================================================
|
|
2733
|
+
clickByText: async (ctx) => {
|
|
2734
|
+
const body = ctx.body;
|
|
2735
|
+
const text = typeof body?.text === "string" ? body.text.trim() : "";
|
|
2736
|
+
if (!text) {
|
|
2737
|
+
return error2("text is required and must not be empty", "INVALID_REQUEST");
|
|
2738
|
+
}
|
|
2739
|
+
const match = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly })[0];
|
|
2740
|
+
if (!match) {
|
|
2741
|
+
return error2(`No element found with text "${text}"`, "ELEMENT_NOT_FOUND");
|
|
2742
|
+
}
|
|
2743
|
+
const response = await executor.executeAction(match.id, { action: "press" });
|
|
2744
|
+
if (!response.success) {
|
|
2745
|
+
return error2(response.error || "Press failed", "ACTION_FAILED");
|
|
2746
|
+
}
|
|
2747
|
+
return success2({
|
|
2748
|
+
clicked: true,
|
|
2749
|
+
element: { id: match.id, type: match.type, label: match.label }
|
|
2750
|
+
});
|
|
2751
|
+
},
|
|
2752
|
+
clickBySelector: async () => {
|
|
2753
|
+
return error2(
|
|
2754
|
+
"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.",
|
|
2755
|
+
"NOT_SUPPORTED"
|
|
2756
|
+
);
|
|
2757
|
+
},
|
|
2758
|
+
typeInto: async (ctx) => {
|
|
2759
|
+
const body = ctx.body;
|
|
2760
|
+
const label = typeof body?.label === "string" ? body.label.trim() : "";
|
|
2761
|
+
if (!label) {
|
|
2762
|
+
return error2(
|
|
2763
|
+
"label is required on native (selector-based typeInto is web-only)",
|
|
2764
|
+
"INVALID_REQUEST"
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
const text = typeof body?.text === "string" ? body.text : "";
|
|
2768
|
+
const match = matchElementsByText(registry, label, { visibleOnly: body?.visibleOnly })[0];
|
|
2769
|
+
if (!match) {
|
|
2770
|
+
return error2(`No input found for label "${label}"`, "ELEMENT_NOT_FOUND");
|
|
2771
|
+
}
|
|
2772
|
+
const response = await executor.executeAction(match.id, {
|
|
2773
|
+
action: "type",
|
|
2774
|
+
params: { text, clear: body?.clear === true }
|
|
2775
|
+
});
|
|
2776
|
+
if (!response.success) {
|
|
2777
|
+
return error2(response.error || "Type failed", "ACTION_FAILED");
|
|
2778
|
+
}
|
|
2779
|
+
return success2({
|
|
2780
|
+
typed: true,
|
|
2781
|
+
element: { id: match.id, type: match.type, label: match.label }
|
|
2782
|
+
});
|
|
2783
|
+
},
|
|
2784
|
+
readValue: async () => {
|
|
2785
|
+
return error2(
|
|
2786
|
+
"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.",
|
|
2787
|
+
"NOT_SUPPORTED"
|
|
2788
|
+
);
|
|
2789
|
+
},
|
|
2790
|
+
findByText: async (ctx) => {
|
|
2791
|
+
const body = ctx.body;
|
|
2792
|
+
const text = typeof body?.text === "string" ? body.text.trim() : "";
|
|
2793
|
+
if (!text) {
|
|
2794
|
+
return error2("text is required and must not be empty", "INVALID_REQUEST");
|
|
2795
|
+
}
|
|
2796
|
+
const matches = matchElementsByText(registry, text, { visibleOnly: body?.visibleOnly });
|
|
2797
|
+
return success2(
|
|
2798
|
+
matches.map((el, index) => ({
|
|
2799
|
+
index,
|
|
2800
|
+
id: el.id,
|
|
2801
|
+
type: el.type,
|
|
2802
|
+
label: el.label
|
|
2803
|
+
}))
|
|
2804
|
+
);
|
|
2805
|
+
},
|
|
2694
2806
|
// Coord-based tap
|
|
2695
2807
|
//
|
|
2696
2808
|
// Synthesizes a press at the given screen coordinates by scanning the
|
|
@@ -3297,6 +3409,17 @@ var WS_ROUTES = [
|
|
|
3297
3409
|
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
3298
3410
|
// matching against registered elements' layout rects.
|
|
3299
3411
|
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
3412
|
+
// App-agnostic interaction parity — mirrors the web/runner convenience
|
|
3413
|
+
// endpoints (`/control/page/click-by-text` etc.) so the SAME cross-platform
|
|
3414
|
+
// contract drives mobile. Text/label-based interactions compose
|
|
3415
|
+
// `executor.find` + `executor.executeAction` over the registry; the
|
|
3416
|
+
// DOM-selector-only variants (`click-by-selector`, `read-value`) have no RN
|
|
3417
|
+
// analog and return a structured NOT_SUPPORTED envelope.
|
|
3418
|
+
{ pattern: "control/page/click-by-text", handler: "clickByText", httpMethods: ["POST"] },
|
|
3419
|
+
{ pattern: "control/page/click-by-selector", handler: "clickBySelector", httpMethods: ["POST"] },
|
|
3420
|
+
{ pattern: "control/page/type-into", handler: "typeInto", httpMethods: ["POST"] },
|
|
3421
|
+
{ pattern: "control/page/read-value", handler: "readValue", httpMethods: ["POST"] },
|
|
3422
|
+
{ pattern: "control/page/find-by-text", handler: "findByText", httpMethods: ["POST"] },
|
|
3300
3423
|
// Test hooks — gated behind `features.testHooks`. Let external runners
|
|
3301
3424
|
// drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
|
|
3302
3425
|
// without mounting React components.
|
|
@@ -3372,6 +3495,11 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
3372
3495
|
aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
|
|
3373
3496
|
aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
|
|
3374
3497
|
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
3498
|
+
clickByText: "App-agnostic: press the first element matching free-text (label/accessibilityLabel/testID): body { text, visibleOnly? }",
|
|
3499
|
+
clickBySelector: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use clickByText or /control/find + /control/element/:id/action.",
|
|
3500
|
+
typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
|
|
3501
|
+
readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
|
|
3502
|
+
findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
|
|
3375
3503
|
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
3376
3504
|
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
3377
3505
|
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|