@deeeed/metamask-harness 0.26.5 → 0.28.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/CHANGELOG.md +17 -0
- package/adapters/mobile/open-device.sh +16 -4
- package/dist/adapters/mobile/prepare.js +114 -4
- package/dist/adapters/mobile/runtime-decision.js +4 -1
- package/dist/adapters.js +8 -6
- package/dist/cli-commands.js +1 -1
- package/dist/command-contract.js +1 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/device-target.js +4 -1
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/mobile-device-view.js +4 -1
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/devices.js +4 -1
- package/dist/doctor.js +25 -2
- package/dist/mm-harness-cli.js +2 -0
- package/dist/recipe-security.js +1 -0
- package/dist/run-recording.js +128 -92
- package/library/actions/extension/platform/cdp.mjs +170 -117
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/mobile/platform/bridge.mjs +14 -5
- package/library/actions/mobile/platform/observe-ui.mjs +437 -0
- package/library/actions/mobile/platform/tool-paths.mjs +122 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/analytics/collector.mjs +50 -5
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +43 -1
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const LOCATOR_LIMIT = 20;
|
|
2
|
+
const INTERACTIVE_ROLE_PARTS = [
|
|
3
|
+
'button',
|
|
4
|
+
'textfield',
|
|
5
|
+
'edittext',
|
|
6
|
+
'searchfield',
|
|
7
|
+
'textarea',
|
|
8
|
+
'checkbox',
|
|
9
|
+
'radiobutton',
|
|
10
|
+
'switch',
|
|
11
|
+
'togglebutton',
|
|
12
|
+
'slider',
|
|
13
|
+
'seekbar',
|
|
14
|
+
'picker',
|
|
15
|
+
'spinner',
|
|
16
|
+
'link',
|
|
17
|
+
'tab',
|
|
18
|
+
'menuitem',
|
|
19
|
+
'cell',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function locatorsFromObservation(observation) {
|
|
23
|
+
const value = record(observation);
|
|
24
|
+
const items = Array.isArray(value.items) ? value.items.map(record) : [];
|
|
25
|
+
const ranked = items
|
|
26
|
+
.filter(isInteractive)
|
|
27
|
+
.map(locatorFor)
|
|
28
|
+
.filter((locator) => locator.suggestions.length > 0)
|
|
29
|
+
.sort((left, right) => {
|
|
30
|
+
const leftBest = left.suggestions[0];
|
|
31
|
+
const rightBest = right.suggestions[0];
|
|
32
|
+
return (
|
|
33
|
+
confidenceRank(leftBest?.confidence) - confidenceRank(rightBest?.confidence) ||
|
|
34
|
+
strategyRank(leftBest?.strategy) - strategyRank(rightBest?.strategy)
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
const locators = ranked.slice(0, LOCATOR_LIMIT);
|
|
38
|
+
return {
|
|
39
|
+
provider: text(value.provider) ?? 'unknown',
|
|
40
|
+
observed: items.length,
|
|
41
|
+
count: locators.length,
|
|
42
|
+
truncated: value.truncated === true || ranked.length > locators.length,
|
|
43
|
+
locators,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function requireVisibleObservation(result) {
|
|
48
|
+
const value = record(result);
|
|
49
|
+
const observations = record(value.observations);
|
|
50
|
+
const visible = observations['ui.visible'];
|
|
51
|
+
if (visible && typeof visible === 'object' && !Array.isArray(visible)) {
|
|
52
|
+
return visible;
|
|
53
|
+
}
|
|
54
|
+
const warning = Array.isArray(value.warnings)
|
|
55
|
+
? value.warnings.map((entry) => text(record(entry).message)).find(Boolean)
|
|
56
|
+
: undefined;
|
|
57
|
+
throw new Error(
|
|
58
|
+
`ui.locators could not read the current ui.visible observation${warning ? `: ${warning}` : '.'}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isInteractive(item) {
|
|
63
|
+
const role = text(item.role)?.toLowerCase() ?? '';
|
|
64
|
+
return (
|
|
65
|
+
Boolean(text(item.selector)) ||
|
|
66
|
+
INTERACTIVE_ROLE_PARTS.some((part) => role.includes(part)) ||
|
|
67
|
+
(item.enabled !== false && Boolean(text(item.test_id)))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function locatorFor(item) {
|
|
72
|
+
const testId = text(item.test_id);
|
|
73
|
+
const label = text(item.label);
|
|
74
|
+
const selector = text(item.selector);
|
|
75
|
+
const role = text(item.role);
|
|
76
|
+
const suggestions = [];
|
|
77
|
+
if (testId) {
|
|
78
|
+
suggestions.push({
|
|
79
|
+
strategy: 'test_id',
|
|
80
|
+
value: testId,
|
|
81
|
+
confidence: 'high',
|
|
82
|
+
params: { test_id: testId },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (label && supportsTextTarget(role)) {
|
|
86
|
+
suggestions.push({
|
|
87
|
+
strategy: 'label',
|
|
88
|
+
value: label,
|
|
89
|
+
confidence: testId ? 'medium' : 'high',
|
|
90
|
+
params: { text: label },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (selector && !selectorRepresentsTestId(selector, testId)) {
|
|
94
|
+
suggestions.push({
|
|
95
|
+
strategy: 'selector',
|
|
96
|
+
value: selector,
|
|
97
|
+
confidence: stableSelector(selector) ? 'high' : 'low',
|
|
98
|
+
params: { selector },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
suggestions.sort((left, right) => (
|
|
102
|
+
confidenceRank(left.confidence) - confidenceRank(right.confidence) ||
|
|
103
|
+
strategyRank(left.strategy) - strategyRank(right.strategy)
|
|
104
|
+
));
|
|
105
|
+
return {
|
|
106
|
+
description: describe(item),
|
|
107
|
+
role,
|
|
108
|
+
enabled: item.enabled !== false,
|
|
109
|
+
bounds: bounds(item.bounds),
|
|
110
|
+
suggestions,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function supportsTextTarget(role) {
|
|
115
|
+
const normalized = role?.toLowerCase() ?? '';
|
|
116
|
+
return ['button', 'link', 'tab', 'menuitem', 'cell'].some((part) =>
|
|
117
|
+
normalized.includes(part));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function describe(item) {
|
|
121
|
+
const parts = [text(item.role) ?? 'element'];
|
|
122
|
+
if (text(item.label)) parts.push(`label=${JSON.stringify(text(item.label))}`);
|
|
123
|
+
if (text(item.test_id)) parts.push(`test_id=${JSON.stringify(text(item.test_id))}`);
|
|
124
|
+
return parts.join(' ');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function bounds(value) {
|
|
128
|
+
const candidate = record(value);
|
|
129
|
+
const keys = ['x', 'y', 'width', 'height'];
|
|
130
|
+
if (!keys.every((key) => Number.isFinite(candidate[key]))) return undefined;
|
|
131
|
+
return Object.fromEntries(keys.map((key) => [key, candidate[key]]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function selectorRepresentsTestId(selector, testId) {
|
|
135
|
+
return Boolean(testId && selector.includes('data-test') && selector.includes(testId));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stableSelector(selector) {
|
|
139
|
+
return /^\[(?:id|data-test(?:id|-id))=/u.test(selector);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function confidenceRank(value) {
|
|
143
|
+
if (value === 'high') return 0;
|
|
144
|
+
if (value === 'medium') return 1;
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function strategyRank(value) {
|
|
149
|
+
if (value === 'test_id') return 0;
|
|
150
|
+
if (value === 'selector') return 1;
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function text(value) {
|
|
155
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function record(value) {
|
|
159
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
160
|
+
}
|
|
@@ -716,6 +716,24 @@
|
|
|
716
716
|
}
|
|
717
717
|
]
|
|
718
718
|
},
|
|
719
|
+
"ui.locators": {
|
|
720
|
+
"description": "List ranked, copy-pasteable ui.press targets from the current ui.visible observation. Prefer high-confidence test IDs when updating durable recipe source after exploratory interaction.",
|
|
721
|
+
"execution_capabilities": [
|
|
722
|
+
"host-read-export"
|
|
723
|
+
],
|
|
724
|
+
"schema": {
|
|
725
|
+
"type": "object",
|
|
726
|
+
"properties": {},
|
|
727
|
+
"additionalProperties": false
|
|
728
|
+
},
|
|
729
|
+
"examples": [
|
|
730
|
+
{
|
|
731
|
+
"action": "ui.locators",
|
|
732
|
+
"intent": "Find stable targets for durable recipe authoring.",
|
|
733
|
+
"next": "done"
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
},
|
|
719
737
|
"ui.screenshot": {
|
|
720
738
|
"description": "Capture registered visual evidence through capture-helper, bounded Chrome-native capture, or an explicitly labeled computed-style DOM raster fallback. Always read the resulting PNG; artifact metadata records the provider.",
|
|
721
739
|
"examples": [
|
|
@@ -734,6 +734,24 @@
|
|
|
734
734
|
}
|
|
735
735
|
]
|
|
736
736
|
},
|
|
737
|
+
"ui.locators": {
|
|
738
|
+
"description": "List ranked, copy-pasteable ui.press targets from the current ui.visible observation. Prefer high-confidence test IDs when updating durable recipe source after exploratory interaction.",
|
|
739
|
+
"execution_capabilities": [
|
|
740
|
+
"host-read-export"
|
|
741
|
+
],
|
|
742
|
+
"schema": {
|
|
743
|
+
"type": "object",
|
|
744
|
+
"properties": {},
|
|
745
|
+
"additionalProperties": false
|
|
746
|
+
},
|
|
747
|
+
"examples": [
|
|
748
|
+
{
|
|
749
|
+
"action": "ui.locators",
|
|
750
|
+
"intent": "Find stable targets for durable recipe authoring.",
|
|
751
|
+
"next": "done"
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
},
|
|
737
755
|
"ui.screenshot": {
|
|
738
756
|
"description": "Capture registered PNG evidence with xcrun simctl on iOS or adb screencap on Android. Artifact metadata records the native provider, command mode, and selected device; invalid or empty PNG output fails before registration.",
|
|
739
757
|
"examples": [
|
|
@@ -3044,5 +3062,29 @@
|
|
|
3044
3062
|
"additionalProperties": false
|
|
3045
3063
|
}
|
|
3046
3064
|
}
|
|
3047
|
-
}
|
|
3065
|
+
},
|
|
3066
|
+
"observers": [
|
|
3067
|
+
{
|
|
3068
|
+
"ref": "ui.screen",
|
|
3069
|
+
"default_for": [
|
|
3070
|
+
"ui.navigate",
|
|
3071
|
+
"ui.press",
|
|
3072
|
+
"ui.screenshot",
|
|
3073
|
+
"ui.scroll",
|
|
3074
|
+
"ui.set_input",
|
|
3075
|
+
"ui.wait_for"
|
|
3076
|
+
]
|
|
3077
|
+
},
|
|
3078
|
+
{
|
|
3079
|
+
"ref": "ui.visible",
|
|
3080
|
+
"default_for": [
|
|
3081
|
+
"ui.navigate",
|
|
3082
|
+
"ui.press",
|
|
3083
|
+
"ui.screenshot",
|
|
3084
|
+
"ui.scroll",
|
|
3085
|
+
"ui.set_input",
|
|
3086
|
+
"ui.wait_for"
|
|
3087
|
+
]
|
|
3088
|
+
}
|
|
3089
|
+
]
|
|
3048
3090
|
}
|