mobile-debug-mcp 0.27.0 → 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/dist/interact/index.js +238 -11
- package/dist/server/common.js +134 -3
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +3 -0
- package/docs/rfcs/{012.md → 012-action-trace-and-xecution-observability.md} +43 -4
- package/docs/specs/mcp-tooling-spec-v1.md +14 -0
- package/docs/tools/interact.md +44 -0
- package/package.json +1 -1
- package/src/interact/index.ts +268 -12
- package/src/server/common.ts +194 -4
- package/src/server-core.ts +1 -1
- package/src/types.ts +23 -0
- package/test/device/manual/observe/rfc012_trace.manual.ts +51 -0
- package/test/unit/interact/expect_tools.test.ts +57 -25
- package/test/unit/server/common.test.ts +24 -0
package/src/server/common.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ActionTrace,
|
|
2
3
|
ActionExecutionResult,
|
|
3
4
|
ActionFailureCode,
|
|
4
5
|
ActionTargetResolved,
|
|
5
6
|
FailureClass,
|
|
6
|
-
RecoveryState
|
|
7
|
+
RecoveryState,
|
|
8
|
+
TraceResult,
|
|
9
|
+
TraceStage,
|
|
10
|
+
TraceStep
|
|
7
11
|
} from '../types.js'
|
|
8
12
|
import { ToolsObserve } from '../observe/index.js'
|
|
9
13
|
|
|
@@ -92,6 +96,172 @@ export async function captureActionFingerprint(platform?: 'android' | 'ios', dev
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
export function createTraceStep({
|
|
100
|
+
stage,
|
|
101
|
+
timestamp,
|
|
102
|
+
result,
|
|
103
|
+
attemptIndex,
|
|
104
|
+
cycleId,
|
|
105
|
+
metadata
|
|
106
|
+
}: {
|
|
107
|
+
stage: TraceStage
|
|
108
|
+
timestamp: number
|
|
109
|
+
result: TraceResult
|
|
110
|
+
attemptIndex: number
|
|
111
|
+
cycleId?: number
|
|
112
|
+
metadata?: Record<string, unknown>
|
|
113
|
+
}): TraceStep {
|
|
114
|
+
return {
|
|
115
|
+
stage,
|
|
116
|
+
timestamp,
|
|
117
|
+
result,
|
|
118
|
+
attempt_index: attemptIndex,
|
|
119
|
+
...(cycleId !== undefined ? { cycle_id: cycleId } : {}),
|
|
120
|
+
...(metadata ? { metadata } : {})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildActionTrace({
|
|
125
|
+
actionId,
|
|
126
|
+
actionType,
|
|
127
|
+
sourceModule,
|
|
128
|
+
selector,
|
|
129
|
+
resolved,
|
|
130
|
+
success,
|
|
131
|
+
failure,
|
|
132
|
+
details,
|
|
133
|
+
recovery,
|
|
134
|
+
attempts = 1,
|
|
135
|
+
steps
|
|
136
|
+
}: {
|
|
137
|
+
actionId: string
|
|
138
|
+
actionType: string
|
|
139
|
+
sourceModule: 'server' | 'interact'
|
|
140
|
+
selector: Record<string, unknown> | null
|
|
141
|
+
resolved?: Partial<ActionTargetResolved> | null
|
|
142
|
+
success: boolean
|
|
143
|
+
failure?: { failureCode: ActionFailureCode; retryable: boolean }
|
|
144
|
+
details?: Record<string, unknown>
|
|
145
|
+
recovery?: RecoveryState
|
|
146
|
+
attempts?: number
|
|
147
|
+
steps?: TraceStep[]
|
|
148
|
+
}): ActionTrace {
|
|
149
|
+
if (steps && steps.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
action_id: actionId,
|
|
152
|
+
steps,
|
|
153
|
+
final_outcome: success ? 'success' : 'failure',
|
|
154
|
+
attempts: Math.max(1, Math.floor(attempts || steps.length))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const start = Date.now()
|
|
159
|
+
const builtSteps: TraceStep[] = []
|
|
160
|
+
let attemptIndex = 0
|
|
161
|
+
const totalAttempts = Math.max(1, Math.floor(attempts || 1))
|
|
162
|
+
|
|
163
|
+
if (selector || resolved) {
|
|
164
|
+
const stageResult: TraceResult = resolved ? 'success' : 'failure'
|
|
165
|
+
builtSteps.push(createTraceStep({
|
|
166
|
+
stage: 'resolve',
|
|
167
|
+
timestamp: start,
|
|
168
|
+
result: stageResult,
|
|
169
|
+
attemptIndex: attemptIndex++,
|
|
170
|
+
metadata: {
|
|
171
|
+
action_type: actionType,
|
|
172
|
+
source_module: sourceModule,
|
|
173
|
+
selector: selector ?? null,
|
|
174
|
+
resolved: resolved ? normalizeResolvedTarget(resolved) : null
|
|
175
|
+
}
|
|
176
|
+
}))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
builtSteps.push(createTraceStep({
|
|
180
|
+
stage: 'execute',
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
result: success ? 'success' : 'failure',
|
|
183
|
+
attemptIndex: attemptIndex++,
|
|
184
|
+
metadata: {
|
|
185
|
+
action_type: actionType,
|
|
186
|
+
source_module: sourceModule,
|
|
187
|
+
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {})
|
|
188
|
+
}
|
|
189
|
+
}))
|
|
190
|
+
|
|
191
|
+
const hasStabilizeDetails = Boolean(details && (
|
|
192
|
+
Object.prototype.hasOwnProperty.call(details, 'stabilization_attempts') ||
|
|
193
|
+
Object.prototype.hasOwnProperty.call(details, 'stable_observation_count') ||
|
|
194
|
+
Object.prototype.hasOwnProperty.call(details, 'snapshot_freshness_ms')
|
|
195
|
+
))
|
|
196
|
+
const hasVerifyDetails = Boolean(details && (
|
|
197
|
+
Object.prototype.hasOwnProperty.call(details, 'within_tolerance') ||
|
|
198
|
+
Object.prototype.hasOwnProperty.call(details, 'converged') ||
|
|
199
|
+
Object.prototype.hasOwnProperty.call(details, 'observed_state')
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
if (hasStabilizeDetails) {
|
|
203
|
+
builtSteps.push(createTraceStep({
|
|
204
|
+
stage: 'stabilize',
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
result: success ? 'success' : 'failure',
|
|
207
|
+
attemptIndex: attemptIndex++,
|
|
208
|
+
metadata: {
|
|
209
|
+
stabilization_attempts: (details?.stabilization_attempts as number | undefined) ?? null,
|
|
210
|
+
stable_observation_count: (details?.stable_observation_count as number | undefined) ?? null,
|
|
211
|
+
snapshot_freshness_ms: (details?.snapshot_freshness_ms as number | undefined) ?? null
|
|
212
|
+
}
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (hasVerifyDetails) {
|
|
217
|
+
builtSteps.push(createTraceStep({
|
|
218
|
+
stage: 'verify',
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
result: success ? 'success' : 'failure',
|
|
221
|
+
attemptIndex: attemptIndex++,
|
|
222
|
+
metadata: {
|
|
223
|
+
within_tolerance: details?.within_tolerance ?? null,
|
|
224
|
+
converged: details?.converged ?? null,
|
|
225
|
+
actual_state: details?.actual_state ?? null,
|
|
226
|
+
reason: details?.reason ?? null
|
|
227
|
+
}
|
|
228
|
+
}))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (failure) {
|
|
232
|
+
builtSteps.push(createTraceStep({
|
|
233
|
+
stage: 'recover',
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
result: failure.retryable ? 'retry' : 'failure',
|
|
236
|
+
attemptIndex: attemptIndex++,
|
|
237
|
+
metadata: {
|
|
238
|
+
failure_class: recovery?.failure_class ?? mapFailureCodeToFailureClass(failure.failureCode),
|
|
239
|
+
runtime_code: failure.failureCode,
|
|
240
|
+
retry_allowed: failure.retryable,
|
|
241
|
+
recovery_attempts: recovery?.recovery_attempts ?? 0,
|
|
242
|
+
retry_depth: recovery?.retry_depth ?? 0
|
|
243
|
+
}
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!builtSteps.length) {
|
|
248
|
+
builtSteps.push(createTraceStep({
|
|
249
|
+
stage: 'execute',
|
|
250
|
+
timestamp: start,
|
|
251
|
+
result: success ? 'success' : 'failure',
|
|
252
|
+
attemptIndex: 0,
|
|
253
|
+
metadata: { action_type: actionType, source_module: sourceModule }
|
|
254
|
+
}))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
action_id: actionId,
|
|
259
|
+
steps: builtSteps,
|
|
260
|
+
final_outcome: success ? 'success' : 'failure',
|
|
261
|
+
attempts: totalAttempts
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
95
265
|
export function normalizeResolvedTarget(value: Partial<ActionTargetResolved> | null = null): ActionTargetResolved | null {
|
|
96
266
|
if (!value) return null
|
|
97
267
|
return {
|
|
@@ -181,7 +351,8 @@ export function buildActionExecutionResult({
|
|
|
181
351
|
uiFingerprintAfter,
|
|
182
352
|
failure,
|
|
183
353
|
details,
|
|
184
|
-
sourceModule
|
|
354
|
+
sourceModule,
|
|
355
|
+
traceSteps
|
|
185
356
|
}: {
|
|
186
357
|
actionType: string
|
|
187
358
|
device?: ActionExecutionResult['device']
|
|
@@ -193,11 +364,17 @@ export function buildActionExecutionResult({
|
|
|
193
364
|
failure?: { failureCode: ActionFailureCode; retryable: boolean }
|
|
194
365
|
details?: Record<string, unknown>
|
|
195
366
|
sourceModule: 'server' | 'interact'
|
|
367
|
+
traceSteps?: TraceStep[]
|
|
196
368
|
}): ActionExecutionResult {
|
|
197
369
|
const timestampMs = Date.now()
|
|
198
370
|
const timestamp = new Date(timestampMs).toISOString()
|
|
371
|
+
const actionId = nextActionId(actionType, timestampMs)
|
|
372
|
+
const recoveryState = failure ? buildRecoveryState(failure.failureCode, failure.retryable) : undefined
|
|
373
|
+
const attempts = typeof details?.attempts === 'number' && Number.isFinite(details.attempts)
|
|
374
|
+
? Math.max(1, Math.floor(details.attempts))
|
|
375
|
+
: 1
|
|
199
376
|
return {
|
|
200
|
-
action_id:
|
|
377
|
+
action_id: actionId,
|
|
201
378
|
timestamp,
|
|
202
379
|
action_type: actionType,
|
|
203
380
|
lifecycle_state: determineActionLifecycleState({ success, failure }),
|
|
@@ -209,7 +386,20 @@ export function buildActionExecutionResult({
|
|
|
209
386
|
},
|
|
210
387
|
success,
|
|
211
388
|
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
|
|
212
|
-
...(
|
|
389
|
+
...(recoveryState ? { recovery: recoveryState } : {}),
|
|
390
|
+
trace: buildActionTrace({
|
|
391
|
+
actionId,
|
|
392
|
+
actionType,
|
|
393
|
+
sourceModule,
|
|
394
|
+
selector,
|
|
395
|
+
resolved,
|
|
396
|
+
success,
|
|
397
|
+
failure,
|
|
398
|
+
details,
|
|
399
|
+
recovery: recoveryState,
|
|
400
|
+
attempts,
|
|
401
|
+
steps: traceSteps
|
|
402
|
+
}),
|
|
213
403
|
ui_fingerprint_before: uiFingerprintBefore,
|
|
214
404
|
ui_fingerprint_after: uiFingerprintAfter,
|
|
215
405
|
...(details ? { details } : {})
|
package/src/server-core.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -113,6 +113,25 @@ export interface UIElementSemanticMetadata {
|
|
|
113
113
|
state_shape?: 'continuous' | 'discrete' | 'semantic' | null;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export type TraceStage = 'resolve' | 'execute' | 'verify' | 'stabilize' | 'recover';
|
|
117
|
+
export type TraceResult = 'success' | 'failure' | 'retry';
|
|
118
|
+
|
|
119
|
+
export interface TraceStep {
|
|
120
|
+
stage: TraceStage;
|
|
121
|
+
timestamp: number;
|
|
122
|
+
result: TraceResult;
|
|
123
|
+
attempt_index: number;
|
|
124
|
+
cycle_id?: number;
|
|
125
|
+
metadata?: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ActionTrace {
|
|
129
|
+
action_id: string;
|
|
130
|
+
steps: TraceStep[];
|
|
131
|
+
final_outcome: 'success' | 'failure';
|
|
132
|
+
attempts: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
export type FailureClass =
|
|
117
136
|
| 'TargetResolutionFailure'
|
|
118
137
|
| 'ExecutionFailure'
|
|
@@ -377,6 +396,7 @@ export interface ActionExecutionResult {
|
|
|
377
396
|
failure_code?: ActionFailureCode;
|
|
378
397
|
retryable?: boolean;
|
|
379
398
|
recovery?: RecoveryState;
|
|
399
|
+
trace: ActionTrace;
|
|
380
400
|
ui_fingerprint_before?: string | null;
|
|
381
401
|
ui_fingerprint_after?: string | null;
|
|
382
402
|
details?: Record<string, unknown>;
|
|
@@ -400,6 +420,7 @@ export interface ExpectScreenResponse {
|
|
|
400
420
|
matched: boolean;
|
|
401
421
|
reason: string;
|
|
402
422
|
};
|
|
423
|
+
trace: ActionTrace;
|
|
403
424
|
}
|
|
404
425
|
|
|
405
426
|
export interface ExpectElementVisibleResponse {
|
|
@@ -423,6 +444,7 @@ export interface ExpectElementVisibleResponse {
|
|
|
423
444
|
reason?: string;
|
|
424
445
|
failure_code?: 'TIMEOUT' | 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
|
|
425
446
|
retryable?: boolean;
|
|
447
|
+
trace: ActionTrace;
|
|
426
448
|
}
|
|
427
449
|
|
|
428
450
|
export interface ExpectStateResponse {
|
|
@@ -451,6 +473,7 @@ export interface ExpectStateResponse {
|
|
|
451
473
|
stabilization_window_ms?: number;
|
|
452
474
|
stable_observation_count?: number;
|
|
453
475
|
snapshot_freshness_ms?: number;
|
|
476
|
+
trace: ActionTrace;
|
|
454
477
|
}
|
|
455
478
|
|
|
456
479
|
export interface AdjustControlResponse extends ActionExecutionResult {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ToolsInteract } from '../../../../src/interact/index.js'
|
|
3
|
+
import { ToolsObserve } from '../../../../src/observe/index.js'
|
|
4
|
+
|
|
5
|
+
async function verifyTrace(platform: 'android' | 'ios', deviceId?: string) {
|
|
6
|
+
const fingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as any
|
|
7
|
+
assert.ok(fingerprint, `${platform}: missing fingerprint response`)
|
|
8
|
+
assert.ok(fingerprint.fingerprint || fingerprint.activity, `${platform}: missing fingerprint or activity`)
|
|
9
|
+
|
|
10
|
+
const expected = fingerprint.fingerprint
|
|
11
|
+
? { fingerprint: fingerprint.fingerprint }
|
|
12
|
+
: { screen: fingerprint.activity }
|
|
13
|
+
|
|
14
|
+
const response = await ToolsInteract.expectScreenHandler({
|
|
15
|
+
platform,
|
|
16
|
+
deviceId,
|
|
17
|
+
...expected
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
assert.strictEqual(response.success, true, `${platform}: expect_screen did not succeed`)
|
|
21
|
+
assert.ok(response.trace, `${platform}: trace missing`)
|
|
22
|
+
assert.strictEqual(response.trace.final_outcome, 'success', `${platform}: trace outcome mismatch`)
|
|
23
|
+
assert.ok(Array.isArray(response.trace.steps) && response.trace.steps.length > 0, `${platform}: trace steps missing`)
|
|
24
|
+
assert.strictEqual(response.trace.steps[0].stage, 'verify', `${platform}: expected verify stage`)
|
|
25
|
+
|
|
26
|
+
console.log(`${platform}: RFC 012 trace verified`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const args = process.argv.slice(2)
|
|
31
|
+
const platform = args[0] as 'android' | 'ios' | undefined
|
|
32
|
+
const deviceId = args[1]
|
|
33
|
+
|
|
34
|
+
if (platform === 'android') {
|
|
35
|
+
await verifyTrace('android', deviceId)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (platform === 'ios') {
|
|
40
|
+
await verifyTrace('ios', deviceId)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await verifyTrace('android')
|
|
45
|
+
await verifyTrace('ios')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch((error) => {
|
|
49
|
+
console.error(error)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
})
|
|
@@ -6,22 +6,23 @@ async function run() {
|
|
|
6
6
|
console.log('Starting expect_* unit tests...')
|
|
7
7
|
const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
8
8
|
const originalGetCurrentScreenHandler = (Observe as any).ToolsObserve.getCurrentScreenHandler
|
|
9
|
+
const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
|
|
9
10
|
const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_home', activity: 'com.example.HomeActivity' })
|
|
13
14
|
let expectScreen = await ToolsInteract.expectScreenHandler({ platform: 'android', fingerprint: 'fp_home' })
|
|
14
|
-
assert.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
reason: 'observed fingerprint matches expected fingerprint fp_home'
|
|
23
|
-
}
|
|
15
|
+
assert.strictEqual(expectScreen.success, true)
|
|
16
|
+
assert.deepStrictEqual(expectScreen.observed_screen, { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' })
|
|
17
|
+
assert.deepStrictEqual(expectScreen.expected_screen, { fingerprint: 'fp_home', screen: null })
|
|
18
|
+
assert.strictEqual(expectScreen.confidence, 1)
|
|
19
|
+
assert.deepStrictEqual(expectScreen.comparison, {
|
|
20
|
+
basis: 'fingerprint',
|
|
21
|
+
matched: true,
|
|
22
|
+
reason: 'observed fingerprint matches expected fingerprint fp_home'
|
|
24
23
|
})
|
|
24
|
+
assert.strictEqual(expectScreen.trace.final_outcome, 'success')
|
|
25
|
+
assert.strictEqual(expectScreen.trace.steps[0].stage, 'verify')
|
|
25
26
|
|
|
26
27
|
;(Observe as any).ToolsObserve.getCurrentScreenHandler = async () => ({
|
|
27
28
|
activity: 'com.example.HomeActivity',
|
|
@@ -58,6 +59,8 @@ async function run() {
|
|
|
58
59
|
assert.strictEqual(expectElementVisible.element?.resource_id, 'rid_ready')
|
|
59
60
|
assert.strictEqual(expectElementVisible.expected_condition, 'visible')
|
|
60
61
|
assert.strictEqual(expectElementVisible.reason, 'selector is visible')
|
|
62
|
+
assert.strictEqual(expectElementVisible.trace.final_outcome, 'success')
|
|
63
|
+
assert.strictEqual(expectElementVisible.trace.steps[0].stage, 'verify')
|
|
61
64
|
|
|
62
65
|
;(ToolsInteract as any).waitForUIHandler = async () => ({
|
|
63
66
|
status: 'timeout',
|
|
@@ -73,27 +76,56 @@ async function run() {
|
|
|
73
76
|
selector: { text: 'Missing' },
|
|
74
77
|
platform: 'android'
|
|
75
78
|
})
|
|
76
|
-
assert.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
assert.strictEqual(timeoutResult.success, false)
|
|
80
|
+
assert.deepStrictEqual(timeoutResult.selector, { text: 'Missing' })
|
|
81
|
+
assert.strictEqual(timeoutResult.element_id, null)
|
|
82
|
+
assert.strictEqual(timeoutResult.expected_condition, 'visible')
|
|
83
|
+
assert.deepStrictEqual(timeoutResult.observed, {
|
|
84
|
+
status: 'timeout',
|
|
85
|
+
matched_count: 0,
|
|
86
|
+
condition_satisfied: false,
|
|
87
|
+
selected_index: null,
|
|
88
|
+
last_matched_element: null
|
|
89
|
+
})
|
|
90
|
+
assert.strictEqual(timeoutResult.reason, 'Condition visible not satisfied within timeout; observed 0 match(es)')
|
|
91
|
+
assert.strictEqual(timeoutResult.failure_code, 'TIMEOUT')
|
|
92
|
+
assert.strictEqual(timeoutResult.retryable, true)
|
|
93
|
+
assert.strictEqual(timeoutResult.trace.final_outcome, 'failure')
|
|
94
|
+
|
|
95
|
+
let uiTreeCallCount = 0
|
|
96
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => {
|
|
97
|
+
uiTreeCallCount++
|
|
98
|
+
return {
|
|
99
|
+
device: { platform: 'android', id: 'emulator-5554' },
|
|
100
|
+
captured_at_ms: Date.now(),
|
|
101
|
+
elements: [{
|
|
102
|
+
elementId: 'el_state',
|
|
103
|
+
text: 'Stateful',
|
|
104
|
+
resource_id: 'rid_state',
|
|
105
|
+
accessibility_id: null,
|
|
106
|
+
class: 'TextView',
|
|
107
|
+
bounds: [0, 0, 10, 10],
|
|
108
|
+
index: 0,
|
|
109
|
+
state: { enabled: true, value: 7 }
|
|
110
|
+
}]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const expectState = await ToolsInteract.expectStateHandler({
|
|
114
|
+
selector: { text: 'Stateful' },
|
|
115
|
+
property: 'value',
|
|
116
|
+
expected: 7,
|
|
117
|
+
platform: 'android'
|
|
91
118
|
})
|
|
119
|
+
assert.strictEqual(expectState.success, true)
|
|
120
|
+
assert.strictEqual(uiTreeCallCount >= 2, true)
|
|
121
|
+
assert.ok(expectState.trace.steps.filter((step) => step.stage === 'stabilize').length >= 2)
|
|
122
|
+
assert.ok(expectState.trace.steps.filter((step) => step.stage === 'verify').length >= 2)
|
|
92
123
|
|
|
93
124
|
console.log('expect_* unit tests passed')
|
|
94
125
|
} finally {
|
|
95
126
|
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
|
|
96
127
|
;(Observe as any).ToolsObserve.getCurrentScreenHandler = originalGetCurrentScreenHandler
|
|
128
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
|
|
97
129
|
;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
|
+
import { createTraceStep } from '../../../src/server/common.js'
|
|
2
3
|
import { buildActionExecutionResult, inferGenericFailure, requireBooleanArg } from '../../../src/server/common.js'
|
|
3
4
|
|
|
4
5
|
function run() {
|
|
@@ -27,6 +28,8 @@ function run() {
|
|
|
27
28
|
assert.strictEqual(recoveryResult.recovery?.retry_allowed, false)
|
|
28
29
|
assert.strictEqual(recoveryResult.recovery?.max_recovery_attempts, 3)
|
|
29
30
|
assert.strictEqual(recoveryResult.recovery?.max_retry_depth, 3)
|
|
31
|
+
assert.strictEqual(recoveryResult.trace.final_outcome, 'failure')
|
|
32
|
+
assert.strictEqual(recoveryResult.trace.steps.at(-1)?.stage, 'recover')
|
|
30
33
|
|
|
31
34
|
const notInteractableResult = buildActionExecutionResult({
|
|
32
35
|
actionType: 'tap',
|
|
@@ -41,6 +44,27 @@ function run() {
|
|
|
41
44
|
assert.strictEqual(notInteractableResult.recovery?.failure_class, 'ExecutionFailure')
|
|
42
45
|
assert.strictEqual(notInteractableResult.recovery?.runtime_code, 'ELEMENT_NOT_INTERACTABLE')
|
|
43
46
|
assert.strictEqual(notInteractableResult.recovery?.retry_allowed, true)
|
|
47
|
+
assert.strictEqual(notInteractableResult.trace.steps[0].stage, 'resolve')
|
|
48
|
+
assert.strictEqual(notInteractableResult.trace.steps[0].result, 'failure')
|
|
49
|
+
assert.strictEqual(notInteractableResult.trace.steps.at(-1)?.stage, 'recover')
|
|
50
|
+
|
|
51
|
+
const traceSteps = [
|
|
52
|
+
createTraceStep({ stage: 'resolve', timestamp: 100, result: 'success', attemptIndex: 0 }),
|
|
53
|
+
createTraceStep({ stage: 'execute', timestamp: 200, result: 'retry', attemptIndex: 1 }),
|
|
54
|
+
createTraceStep({ stage: 'verify', timestamp: 300, result: 'success', attemptIndex: 2 })
|
|
55
|
+
]
|
|
56
|
+
const tracedResult = buildActionExecutionResult({
|
|
57
|
+
actionType: 'tap',
|
|
58
|
+
sourceModule: 'server',
|
|
59
|
+
selector: { x: 1, y: 1 },
|
|
60
|
+
success: true,
|
|
61
|
+
uiFingerprintBefore: 'before',
|
|
62
|
+
uiFingerprintAfter: 'after',
|
|
63
|
+
details: { attempts: 3 },
|
|
64
|
+
traceSteps
|
|
65
|
+
})
|
|
66
|
+
assert.deepStrictEqual(tracedResult.trace.steps, traceSteps)
|
|
67
|
+
assert.strictEqual(tracedResult.trace.attempts, 3)
|
|
44
68
|
|
|
45
69
|
console.log('server common tests passed')
|
|
46
70
|
}
|