peachsim 0.1.656 → 0.1.658
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 +22 -3
- package/cli/app-config.ts +34 -15
- package/cli/app-project.ts +6 -6
- package/cli/bridge-flow-runner.ts +10 -23
- package/cli/commands/compat.ts +234 -10
- package/cli/commands/control.ts +20 -20
- package/cli/commands/do-chain.ts +5 -1
- package/cli/commands/flow.ts +3 -3
- package/cli/commands/inspect/actions.ts +33 -76
- package/cli/commands/inspect/resolve-target.ts +67 -2
- package/cli/commands/inspect.ts +16 -1
- package/cli/drivers/playwright-provisioning.ts +95 -0
- package/cli/ws-bridge.ts +7 -7
- package/detox/config-loader.ts +1 -1
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +9 -1
- package/dist-lib/bridge-contract-input.mjs +9 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +27 -26
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +90 -47
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +51 -8
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +317 -70
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +8 -4
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +300 -29
- package/dist-lib/native-app-identity.cjs +58 -25
- package/dist-lib/native-app-identity.mjs +57 -25
- package/dist-lib/plist.cjs +64 -0
- package/dist-lib/plist.mjs +29 -0
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +14 -16
- package/dist-lib/sdk.cjs +36 -49
- package/dist-lib/sdk.mjs +36 -49
- package/dist-lib/skills.cjs +341 -198
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +51 -8
- package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
- package/kotlin/AppSources/android/content/Context.kt +9 -2
- package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
- package/kotlin/AppSources/android/graphics/Color.kt +13 -0
- package/kotlin/AppSources/android/view/View.kt +2 -0
- package/kotlin/AppSources/android/view/Window.kt +99 -8
- package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
- package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
- package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
- package/package.json +8 -1
- package/scripts/dev-server-scanner.ts +3 -3
- package/scripts/optional-demo-registry.ts +1 -1
- package/skills/peach-setup/SKILL.md +27 -0
- package/src/android-theme.ts +167 -0
- package/src/android-xml.ts +70 -0
- package/src/bridge-contract-input.ts +10 -1
- package/src/bridge-contract.ts +7 -1
- package/src/config.ts +34 -26
- package/src/generated/android-framework-themes.json +6303 -0
- package/src/host/replacement-module-handler.ts +33 -0
- package/src/index.ts +1 -0
- package/src/kotlin-app.ts +75 -61
- package/src/kotlin-host.ts +7 -3
- package/src/metro-plugin.ts +46 -3
- package/src/native-app-identity.ts +75 -26
- package/src/sim-client.ts +2 -4
package/cli/commands/do-chain.ts
CHANGED
|
@@ -313,7 +313,7 @@ export async function runDoChain(
|
|
|
313
313
|
const { SimPerformError } = simClientModule
|
|
314
314
|
const { checkSimHealth, parseBridgeCliArgs } = bridgeModule
|
|
315
315
|
const { isAgentEnv } = envModule
|
|
316
|
-
const { resolveTargetCoords } = targetModule
|
|
316
|
+
const { offscreenTargetRefusal, resolveTargetCoords } = targetModule
|
|
317
317
|
const wantsJson = args.includes('--json')
|
|
318
318
|
const skipSettle = args.includes('--no-wait') || process.env.PEACH_NO_AUTO_WAIT === '1'
|
|
319
319
|
const parsed = parseBridgeCliArgs(args, {
|
|
@@ -355,6 +355,10 @@ export async function runDoChain(
|
|
|
355
355
|
`do chain could not find testID ${JSON.stringify(action.id)}`,
|
|
356
356
|
)
|
|
357
357
|
}
|
|
358
|
+
const refusal = offscreenTargetRefusal(sim.plane, target)
|
|
359
|
+
if (refusal) {
|
|
360
|
+
throw new BridgeInputError(`do chain #${action.id}: ${refusal}`)
|
|
361
|
+
}
|
|
358
362
|
steps.push(
|
|
359
363
|
action.type === 'targetedDoubleTap'
|
|
360
364
|
? {
|
package/cli/commands/flow.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { tmpdir } from 'os'
|
|
|
7
7
|
import * as path from 'path'
|
|
8
8
|
import { scanDevServers } from '../../scripts/dev-server-scanner'
|
|
9
9
|
import { loadOptionalDemoApps } from '../../scripts/optional-demo-registry'
|
|
10
|
-
import {
|
|
10
|
+
import { applyPeachConfigToUrl, type PeachConfig } from '../../src/config'
|
|
11
11
|
import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
|
|
12
12
|
import {
|
|
13
13
|
SootSimBridgeFlowRunner,
|
|
@@ -176,7 +176,7 @@ export function getLastFlowTraceSteps(): SootSimFlowTraceStep[] {
|
|
|
176
176
|
// port and runtime config into the open command.
|
|
177
177
|
export async function resolveAppFrontmatterTarget(
|
|
178
178
|
raw: string,
|
|
179
|
-
): Promise<{ target: string; runtimeConfig?:
|
|
179
|
+
): Promise<{ target: string; runtimeConfig?: PeachConfig }> {
|
|
180
180
|
if (!raw) return { target: '' }
|
|
181
181
|
const trimmed = raw.trim()
|
|
182
182
|
if (!trimmed) return { target: '' }
|
|
@@ -531,7 +531,7 @@ flow extension:
|
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
if (openTarget && targetDevice) {
|
|
534
|
-
const shellBaseUrl =
|
|
534
|
+
const shellBaseUrl = applyPeachConfigToUrl(
|
|
535
535
|
pinnedShellBaseUrl || resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
|
|
536
536
|
targetAppResolution.runtimeConfig,
|
|
537
537
|
)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { inspectTree, type InspectBridge } from './core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isOffscreen,
|
|
4
|
+
isScreenSize,
|
|
5
|
+
resolveTargetCoords,
|
|
6
|
+
UNREPORTED_SCREEN_ERROR,
|
|
7
|
+
type ResolvedTargetCoords,
|
|
8
|
+
} from './resolve-target'
|
|
3
9
|
import { waitForSootsimIdle } from './settling'
|
|
4
10
|
import type { SimSemanticNode } from '../../../src/bridge-contract'
|
|
5
11
|
import type {
|
|
@@ -39,54 +45,6 @@ export interface TapTimingOptions {
|
|
|
39
45
|
retryWaitMs?: number
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
type ScreenSize = { width: number; height: number }
|
|
43
|
-
|
|
44
|
-
function isScreenSize(value: unknown): value is ScreenSize {
|
|
45
|
-
if (value === null || typeof value !== 'object') return false
|
|
46
|
-
const width = Reflect.get(value, 'width')
|
|
47
|
-
const height = Reflect.get(value, 'height')
|
|
48
|
-
return (
|
|
49
|
-
typeof width === 'number' &&
|
|
50
|
-
Number.isFinite(width) &&
|
|
51
|
-
width > 0 &&
|
|
52
|
-
typeof height === 'number' &&
|
|
53
|
-
Number.isFinite(height) &&
|
|
54
|
-
height > 0
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function viewportFromCapture(value: unknown): unknown {
|
|
59
|
-
if (value === null || typeof value !== 'object') return undefined
|
|
60
|
-
const tree = Reflect.get(value, 'tree')
|
|
61
|
-
return tree !== null && typeof tree === 'object'
|
|
62
|
-
? Reflect.get(tree, 'viewport')
|
|
63
|
-
: undefined
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// the semantic tree has no covering root, so its first node is often a 64x64
|
|
67
|
-
// icon. capture.tree.viewport is the device layout size on local and cloud.
|
|
68
|
-
//
|
|
69
|
-
// a whole screen capture is the only way to ask for it today, and it is not
|
|
70
|
-
// cheap: the headless runtime forces a full redraw of the tree before it walks
|
|
71
|
-
// it, which measured 3.9-5.5s on a real app in a cloud box against 120-290ms
|
|
72
|
-
// for every other command. that is why this runs on the failure path only.
|
|
73
|
-
async function readScreen(bridge: InspectBridge): Promise<ScreenSize | null> {
|
|
74
|
-
try {
|
|
75
|
-
const viewport = viewportFromCapture(
|
|
76
|
-
await bridge.send({ type: 'capture', includeVisual: false }),
|
|
77
|
-
)
|
|
78
|
-
return isScreenSize(viewport)
|
|
79
|
-
? { width: viewport.width, height: viewport.height }
|
|
80
|
-
: null
|
|
81
|
-
} catch {
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isOffscreen(cx: number, cy: number, screen: ScreenSize): boolean {
|
|
87
|
-
return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
48
|
const DEFAULT_AGENT_TIMING: Required<TapTimingOptions> = {
|
|
91
49
|
initialWaitMs: 3000,
|
|
92
50
|
deadlineMs: 5000,
|
|
@@ -185,19 +143,24 @@ export async function tapResolvedTarget(
|
|
|
185
143
|
|
|
186
144
|
const deadline = Date.now() + timing.deadlineMs
|
|
187
145
|
let lastOffscreenAt: string | null = null
|
|
188
|
-
// a cloud sim refuses a point outside its screen itself, and says so, so
|
|
189
|
-
// asking it for a capture first would buy nothing. every other plane still
|
|
190
|
-
// presses whatever the hit test finds at tree coordinates no finger can
|
|
191
|
-
// reach, so there the screen has to be read before dispatching.
|
|
192
|
-
const screen = bridge.plane === 'cloud' ? null : await readScreen(bridge)
|
|
193
146
|
while (Date.now() <= deadline || attempts === 0) {
|
|
194
147
|
attempts++
|
|
195
|
-
const resolved = await args.resolve()
|
|
148
|
+
const { screen, ...resolved } = (await args.resolve()) ?? {}
|
|
149
|
+
// a cloud sim refuses a point outside its screen itself, and says so. every
|
|
150
|
+
// other plane still presses whatever the hit test finds at tree
|
|
151
|
+
// coordinates no finger can reach, so the resolved target's screen decides
|
|
152
|
+
// before dispatching. a runtime too old to report it cannot be checked; a
|
|
153
|
+
// zero screen is one not laid out yet, so the attempt waits and retries.
|
|
154
|
+
const checksScreen = bridge.plane !== 'cloud' && typeof resolved.cx === 'number'
|
|
155
|
+
if (checksScreen && screen === undefined) {
|
|
156
|
+
throw new Error(UNREPORTED_SCREEN_ERROR)
|
|
157
|
+
}
|
|
158
|
+
const unsized = checksScreen && !isScreenSize(screen)
|
|
196
159
|
const payload =
|
|
197
|
-
|
|
160
|
+
checksScreen &&
|
|
198
161
|
typeof resolved.cx === 'number' &&
|
|
199
162
|
typeof resolved.cy === 'number' &&
|
|
200
|
-
screen &&
|
|
163
|
+
isScreenSize(screen) &&
|
|
201
164
|
isOffscreen(resolved.cx, resolved.cy, screen)
|
|
202
165
|
? { ...resolved, offscreen: true, screen }
|
|
203
166
|
: resolved
|
|
@@ -215,7 +178,10 @@ export async function tapResolvedTarget(
|
|
|
215
178
|
// don't dispatch a point outside the device screen: the press would do
|
|
216
179
|
// nothing a user could reproduce. report the offscreen miss instead so
|
|
217
180
|
// drivers scroll first rather than getting stuck trusting a phantom hit.
|
|
218
|
-
if (
|
|
181
|
+
if (unsized) {
|
|
182
|
+
lastResult = { hit: false, reason: 'screen-not-laid-out' }
|
|
183
|
+
lastOffscreenAt = null
|
|
184
|
+
} else if (payload.offscreen) {
|
|
219
185
|
lastResult = {
|
|
220
186
|
hit: false,
|
|
221
187
|
reason: 'offscreen',
|
|
@@ -310,6 +276,7 @@ function payloadFromResolved(
|
|
|
310
276
|
return {
|
|
311
277
|
cx: resolved.x,
|
|
312
278
|
cy: resolved.y,
|
|
279
|
+
screen: resolved.viewport,
|
|
313
280
|
match: {
|
|
314
281
|
nodeId: match.nodeId ?? null,
|
|
315
282
|
id: match.testID ?? null,
|
|
@@ -489,25 +456,14 @@ async function resolveTextTapTarget(
|
|
|
489
456
|
if (!picked) return { matched: 0, total }
|
|
490
457
|
const node = picked.node
|
|
491
458
|
// keep the selected handle: re-searching its text or testID can pick a
|
|
492
|
-
// different control, including a covered tenant behind native UI.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
459
|
+
// different control, including a covered tenant behind native UI. resolve by
|
|
460
|
+
// node is a read, so a remote simulator's driving token can send it.
|
|
461
|
+
const resolved = await resolveTargetCoords(bridge, {
|
|
462
|
+
mode: 'node',
|
|
463
|
+
nodeId: node.nodeId,
|
|
497
464
|
})
|
|
498
|
-
if (!
|
|
499
|
-
return payloadFromResolved(
|
|
500
|
-
{
|
|
501
|
-
x: tap.cx,
|
|
502
|
-
y: tap.cy,
|
|
503
|
-
nodeId: tap.target.nodeId,
|
|
504
|
-
testID: tap.target.testID,
|
|
505
|
-
text: tap.target.text ?? tap.target.accessibilityLabel,
|
|
506
|
-
type: tap.target.type,
|
|
507
|
-
},
|
|
508
|
-
node,
|
|
509
|
-
{ total, idx },
|
|
510
|
-
)
|
|
465
|
+
if (!resolved) return { matched: 0, total }
|
|
466
|
+
return payloadFromResolved(resolved, node, { total, idx })
|
|
511
467
|
}
|
|
512
468
|
|
|
513
469
|
export type TapQuery =
|
|
@@ -567,6 +523,7 @@ export async function tapByQuery(
|
|
|
567
523
|
testID: tap.target.testID,
|
|
568
524
|
text: tap.target.text ?? tap.target.accessibilityLabel,
|
|
569
525
|
type: tap.target.type,
|
|
526
|
+
viewport: tap.viewport,
|
|
570
527
|
},
|
|
571
528
|
{
|
|
572
529
|
nodeId: node.nodeId,
|
|
@@ -35,16 +35,69 @@ export interface ResolvedTargetCoords {
|
|
|
35
35
|
testID?: string | null
|
|
36
36
|
text?: string | null
|
|
37
37
|
type?: string | null
|
|
38
|
+
// absent from runtimes older than the field; coordinate-only callers do not
|
|
39
|
+
// need it, and the tap boundary refuses to guess without it.
|
|
40
|
+
viewport?: { width: number; height: number }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ScreenSize = { width: number; height: number }
|
|
44
|
+
|
|
45
|
+
// a reported screen with a zero or non-finite side has not been laid out yet.
|
|
46
|
+
export function isScreenSize(value: unknown): value is ScreenSize {
|
|
47
|
+
if (value === null || typeof value !== 'object') return false
|
|
48
|
+
const width = Reflect.get(value, 'width')
|
|
49
|
+
const height = Reflect.get(value, 'height')
|
|
50
|
+
return (
|
|
51
|
+
typeof width === 'number' &&
|
|
52
|
+
Number.isFinite(width) &&
|
|
53
|
+
width > 0 &&
|
|
54
|
+
typeof height === 'number' &&
|
|
55
|
+
Number.isFinite(height) &&
|
|
56
|
+
height > 0
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isOffscreen(
|
|
61
|
+
x: number,
|
|
62
|
+
y: number,
|
|
63
|
+
screen: { width: number; height: number },
|
|
64
|
+
): boolean {
|
|
65
|
+
return !(x >= 0 && y >= 0 && x <= screen.width && y <= screen.height)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const UNREPORTED_SCREEN_ERROR =
|
|
69
|
+
'this runtime does not report the screen size a targeted gesture needs; ' +
|
|
70
|
+
'update it with `peach runtime install`, or use coordinates'
|
|
71
|
+
|
|
72
|
+
// a gesture at a resolved point outside the device screen presses whatever the
|
|
73
|
+
// hit test finds where no finger can reach. a cloud sim refuses such a point
|
|
74
|
+
// itself; every other plane checks here. returns the refusal, or null to send.
|
|
75
|
+
export function offscreenTargetRefusal(
|
|
76
|
+
plane: InspectBridge['plane'],
|
|
77
|
+
target: ResolvedTargetCoords,
|
|
78
|
+
): string | null {
|
|
79
|
+
if (plane === 'cloud') return null
|
|
80
|
+
const screen = target.viewport
|
|
81
|
+
if (!screen) return UNREPORTED_SCREEN_ERROR
|
|
82
|
+
if (!isScreenSize(screen)) {
|
|
83
|
+
return 'the screen is not laid out yet, so the target cannot be checked; run the command again'
|
|
84
|
+
}
|
|
85
|
+
if (!isOffscreen(target.x, target.y, screen)) return null
|
|
86
|
+
return `the target at (${Math.round(target.x)},${Math.round(target.y)}) is outside the ${screen.width}x${screen.height} screen; scroll it into view first`
|
|
38
87
|
}
|
|
39
88
|
|
|
40
89
|
export async function resolveTargetCoords(
|
|
41
90
|
bridge: InspectBridge,
|
|
42
|
-
target: { mode: 'testid' | 'text'; value: string },
|
|
91
|
+
target: { mode: 'testid' | 'text'; value: string } | { mode: 'node'; nodeId: number },
|
|
43
92
|
): Promise<ResolvedTargetCoords | null> {
|
|
44
93
|
const result: unknown = await bridge.send({
|
|
45
94
|
type: 'resolve',
|
|
46
95
|
selector:
|
|
47
|
-
target.mode === '
|
|
96
|
+
target.mode === 'node'
|
|
97
|
+
? { nodeId: target.nodeId }
|
|
98
|
+
: target.mode === 'testid'
|
|
99
|
+
? { testID: target.value }
|
|
100
|
+
: { text: target.value },
|
|
48
101
|
})
|
|
49
102
|
if (result === null) return null
|
|
50
103
|
if (typeof result !== 'object') throw new Error('resolve returned an invalid result')
|
|
@@ -52,6 +105,15 @@ export async function resolveTargetCoords(
|
|
|
52
105
|
const resolvedTarget = Reflect.get(result, 'target')
|
|
53
106
|
const semanticNodes = [match, resolvedTarget]
|
|
54
107
|
const point = Reflect.get(result, 'point')
|
|
108
|
+
const viewport = Reflect.get(result, 'viewport')
|
|
109
|
+
const width =
|
|
110
|
+
viewport !== null && typeof viewport === 'object'
|
|
111
|
+
? Reflect.get(viewport, 'width')
|
|
112
|
+
: undefined
|
|
113
|
+
const height =
|
|
114
|
+
viewport !== null && typeof viewport === 'object'
|
|
115
|
+
? Reflect.get(viewport, 'height')
|
|
116
|
+
: undefined
|
|
55
117
|
const x =
|
|
56
118
|
point !== null && typeof point === 'object' ? Reflect.get(point, 'x') : undefined
|
|
57
119
|
const y =
|
|
@@ -75,5 +137,8 @@ export async function resolveTargetCoords(
|
|
|
75
137
|
text:
|
|
76
138
|
semanticMatch.text ?? (target.mode === 'text' ? target.value : semanticMatch.label),
|
|
77
139
|
type: semanticMatch.type,
|
|
140
|
+
...(typeof width === 'number' && typeof height === 'number'
|
|
141
|
+
? { viewport: { width, height } }
|
|
142
|
+
: {}),
|
|
78
143
|
}
|
|
79
144
|
}
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -75,7 +75,11 @@ import {
|
|
|
75
75
|
redactKeyboardStateForOutput,
|
|
76
76
|
redactTextForKeyboardState,
|
|
77
77
|
} from './inspect/redaction'
|
|
78
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
offscreenTargetRefusal,
|
|
80
|
+
readTargetFlag,
|
|
81
|
+
resolveTargetCoords,
|
|
82
|
+
} from './inspect/resolve-target'
|
|
79
83
|
import { runScreensSubcommand } from './inspect/screens'
|
|
80
84
|
import { runSettleSubcommand } from './inspect/settle'
|
|
81
85
|
import { waitForSootsimIdle } from './inspect/settling'
|
|
@@ -2006,6 +2010,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2006
2010
|
return {
|
|
2007
2011
|
cx: resolved.x,
|
|
2008
2012
|
cy: resolved.y,
|
|
2013
|
+
screen: resolved.viewport,
|
|
2009
2014
|
match: {
|
|
2010
2015
|
nodeId: resolved.nodeId,
|
|
2011
2016
|
testID:
|
|
@@ -2798,6 +2803,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2798
2803
|
await reportMissingTarget(bridge, 'double-tap', target)
|
|
2799
2804
|
rnxExit(1)
|
|
2800
2805
|
}
|
|
2806
|
+
const refusal = offscreenTargetRefusal(bridge.plane, resolved)
|
|
2807
|
+
if (refusal) {
|
|
2808
|
+
console.error(` double-tap: ${refusal}`)
|
|
2809
|
+
rnxExit(1)
|
|
2810
|
+
}
|
|
2801
2811
|
x = resolved.x
|
|
2802
2812
|
y = resolved.y
|
|
2803
2813
|
}
|
|
@@ -2838,6 +2848,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2838
2848
|
await reportMissingTarget(bridge, 'long-press', target)
|
|
2839
2849
|
rnxExit(1)
|
|
2840
2850
|
}
|
|
2851
|
+
const refusal = offscreenTargetRefusal(bridge.plane, resolved)
|
|
2852
|
+
if (refusal) {
|
|
2853
|
+
console.error(` long-press: ${refusal}`)
|
|
2854
|
+
rnxExit(1)
|
|
2855
|
+
}
|
|
2841
2856
|
x = resolved.x
|
|
2842
2857
|
y = resolved.y
|
|
2843
2858
|
resolvedTarget = {
|
|
@@ -161,6 +161,8 @@ export function ensurePlaywrightBrowser(
|
|
|
161
161
|
const before = probeBrowser(resolved, probeEnv)
|
|
162
162
|
if (!before.ok) return before
|
|
163
163
|
if (before.exists) {
|
|
164
|
+
const missing = missingLibrariesMessage(before.executablePath, browser)
|
|
165
|
+
if (missing) return { ok: false, message: missing }
|
|
164
166
|
return { ok: true, executablePath: before.executablePath, installed: false }
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -209,5 +211,98 @@ export function ensurePlaywrightBrowser(
|
|
|
209
211
|
`${browser} executable is still missing at ${after.executablePath}`,
|
|
210
212
|
}
|
|
211
213
|
}
|
|
214
|
+
const missing = missingLibrariesMessage(after.executablePath, browser)
|
|
215
|
+
if (missing) return { ok: false, message: missing }
|
|
212
216
|
return { ok: true, executablePath: after.executablePath, installed: true }
|
|
213
217
|
}
|
|
218
|
+
|
|
219
|
+
// the debian package providing each shared library chromium and its headless
|
|
220
|
+
// shell link (readelf NEEDED, linux x64 and arm64), apart from glibc's own.
|
|
221
|
+
const LIBRARY_PACKAGES: Record<string, string> = {
|
|
222
|
+
'libasound.so.2': 'libasound2',
|
|
223
|
+
'libatk-1.0.so.0': 'libatk1.0-0',
|
|
224
|
+
'libatk-bridge-2.0.so.0': 'libatk-bridge2.0-0',
|
|
225
|
+
'libatspi.so.0': 'libatspi2.0-0',
|
|
226
|
+
'libcairo.so.2': 'libcairo2',
|
|
227
|
+
'libcups.so.2': 'libcups2',
|
|
228
|
+
'libdbus-1.so.3': 'libdbus-1-3',
|
|
229
|
+
'libexpat.so.1': 'libexpat1',
|
|
230
|
+
'libgbm.so.1': 'libgbm1',
|
|
231
|
+
'libgio-2.0.so.0': 'libglib2.0-0',
|
|
232
|
+
'libglib-2.0.so.0': 'libglib2.0-0',
|
|
233
|
+
'libgobject-2.0.so.0': 'libglib2.0-0',
|
|
234
|
+
'libnspr4.so': 'libnspr4',
|
|
235
|
+
'libnss3.so': 'libnss3',
|
|
236
|
+
'libnssutil3.so': 'libnss3',
|
|
237
|
+
'libpango-1.0.so.0': 'libpango-1.0-0',
|
|
238
|
+
'libsmime3.so': 'libnss3',
|
|
239
|
+
'libudev.so.1': 'libudev1',
|
|
240
|
+
'libX11.so.6': 'libx11-6',
|
|
241
|
+
'libxcb.so.1': 'libxcb1',
|
|
242
|
+
'libXcomposite.so.1': 'libxcomposite1',
|
|
243
|
+
'libXdamage.so.1': 'libxdamage1',
|
|
244
|
+
'libXext.so.6': 'libxext6',
|
|
245
|
+
'libXfixes.so.3': 'libxfixes3',
|
|
246
|
+
'libxkbcommon.so.0': 'libxkbcommon0',
|
|
247
|
+
'libXrandr.so.2': 'libxrandr2',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// packages renamed by the 64-bit time_t transition in ubuntu 24.04 and debian 13.
|
|
251
|
+
const TIME64_PACKAGES = new Set([
|
|
252
|
+
'libasound2',
|
|
253
|
+
'libatk1.0-0',
|
|
254
|
+
'libatk-bridge2.0-0',
|
|
255
|
+
'libatspi2.0-0',
|
|
256
|
+
'libcups2',
|
|
257
|
+
'libglib2.0-0',
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
function isTime64Distro(): boolean {
|
|
261
|
+
if (!existsSync('/etc/os-release')) return false
|
|
262
|
+
const fields = Object.fromEntries(
|
|
263
|
+
readFileSync('/etc/os-release', 'utf8')
|
|
264
|
+
.split('\n')
|
|
265
|
+
.map((line) => line.match(/^(\w+)="?([^"]*)"?$/))
|
|
266
|
+
.filter((match) => match !== null)
|
|
267
|
+
.map((match) => [match[1], match[2]]),
|
|
268
|
+
)
|
|
269
|
+
const version = Number.parseFloat(fields.VERSION_ID ?? '')
|
|
270
|
+
return (
|
|
271
|
+
(fields.ID === 'ubuntu' && version >= 24.04) ||
|
|
272
|
+
(fields.ID === 'debian' && version >= 13)
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** a linux browser whose shared libraries are missing exits 127 before
|
|
277
|
+
* playwright can say why, so name them and their packages up front, as
|
|
278
|
+
* playwright's own host validation does. */
|
|
279
|
+
function missingLibrariesMessage(executablePath: string, browser: string): string | null {
|
|
280
|
+
if (process.platform !== 'linux') return null
|
|
281
|
+
const ldd = spawnSync('ldd', [executablePath], { encoding: 'utf8', timeout: 30_000 })
|
|
282
|
+
const libraries = [...(ldd.stdout ?? '').matchAll(/^\s*(\S+) => not found$/gm)].map(
|
|
283
|
+
(match) => match[1]!,
|
|
284
|
+
)
|
|
285
|
+
if (!libraries.length) return null
|
|
286
|
+
const time64 = isTime64Distro()
|
|
287
|
+
const packages = new Set<string>()
|
|
288
|
+
const unknown: string[] = []
|
|
289
|
+
for (const library of libraries) {
|
|
290
|
+
const name = LIBRARY_PACKAGES[library]
|
|
291
|
+
if (!name) unknown.push(library)
|
|
292
|
+
else packages.add(time64 && TIME64_PACKAGES.has(name) ? `${name}t64` : name)
|
|
293
|
+
}
|
|
294
|
+
const sudo = process.getuid?.() === 0 ? '' : 'sudo '
|
|
295
|
+
return [
|
|
296
|
+
`${browser} at ${executablePath} cannot start: this system is missing shared libraries it links (${libraries.join(', ')}).`,
|
|
297
|
+
...(packages.size
|
|
298
|
+
? [
|
|
299
|
+
'On Debian or Ubuntu, install them and run the command again:',
|
|
300
|
+
'',
|
|
301
|
+
` ${sudo}apt-get update && ${sudo}apt-get install -y --no-install-recommends ${[...packages].join(' ')}`,
|
|
302
|
+
]
|
|
303
|
+
: []),
|
|
304
|
+
...(unknown.length
|
|
305
|
+
? ['', `No known Debian package provides: ${unknown.join(', ')}`]
|
|
306
|
+
: []),
|
|
307
|
+
].join('\n')
|
|
308
|
+
}
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
} from '../src/home-paths'
|
|
25
25
|
import { createCloudBridgeForParsed } from './cloud-client'
|
|
26
26
|
import {
|
|
27
|
-
clearCurrentSimId,
|
|
28
27
|
getCliIdentity,
|
|
29
28
|
readCurrentSim,
|
|
30
29
|
readCurrentSimId,
|
|
@@ -825,13 +824,14 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
825
824
|
)
|
|
826
825
|
return sendOnce(successor)
|
|
827
826
|
}
|
|
828
|
-
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
827
|
+
// the saved sim is not connected and nothing proves what replaced
|
|
828
|
+
// it. fail closed instead of driving whichever sim is primary; that
|
|
829
|
+
// is how agents end up recording the wrong app. the pin stays: a
|
|
830
|
+
// page whose socket dropped reconnects under the same id, and
|
|
831
|
+
// clearing the pin then would hand every later command to primary.
|
|
832
832
|
throw new Error(
|
|
833
|
-
`saved sim ${sentSimId} is
|
|
834
|
-
'run `peach list` and ' +
|
|
833
|
+
`saved sim ${sentSimId} is not connected${message.slice(missingSimMessage.length)}; ` +
|
|
834
|
+
'commands resume when it reconnects, or run `peach list` and ' +
|
|
835
835
|
`\`peach use <sim>\`, or open a fresh sim`,
|
|
836
836
|
)
|
|
837
837
|
}
|
package/detox/config-loader.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function mapDetoxDevice(config: DetoxConfig | null): string {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// generate a sootsim-compatible config from detox config
|
|
124
|
-
export function
|
|
124
|
+
export function toPeachConfig(
|
|
125
125
|
projectDir: string,
|
|
126
126
|
config: DetoxConfig | null,
|
|
127
127
|
): SootSimDetoxConfig {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -358,6 +358,14 @@ function semanticResolveSelector(value, path) {
|
|
|
358
358
|
return validationFailure(path, "must select testID or text");
|
|
359
359
|
}
|
|
360
360
|
function parseSemanticResolveSelector(value) {
|
|
361
|
+
const selector = recordValue(value, "selector");
|
|
362
|
+
if (selector.nodeId !== void 0) {
|
|
363
|
+
const nodeId = numberValue(selector.nodeId, "selector.nodeId");
|
|
364
|
+
if (!Number.isSafeInteger(nodeId)) {
|
|
365
|
+
return validationFailure("selector.nodeId", "must be an integer");
|
|
366
|
+
}
|
|
367
|
+
return { nodeId };
|
|
368
|
+
}
|
|
361
369
|
return semanticResolveSelector(value, "selector");
|
|
362
370
|
}
|
|
363
371
|
function parseSemanticWaitOptions(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
|
|
3
3
|
// src/bridge-contract.ts
|
|
4
4
|
var SIM_LONG_PRESS_MAX_MS = 5e3;
|
|
@@ -322,6 +322,14 @@ function semanticResolveSelector(value, path) {
|
|
|
322
322
|
return validationFailure(path, "must select testID or text");
|
|
323
323
|
}
|
|
324
324
|
function parseSemanticResolveSelector(value) {
|
|
325
|
+
const selector = recordValue(value, "selector");
|
|
326
|
+
if (selector.nodeId !== void 0) {
|
|
327
|
+
const nodeId = numberValue(selector.nodeId, "selector.nodeId");
|
|
328
|
+
if (!Number.isSafeInteger(nodeId)) {
|
|
329
|
+
return validationFailure("selector.nodeId", "must be an integer");
|
|
330
|
+
}
|
|
331
|
+
return { nodeId };
|
|
332
|
+
}
|
|
325
333
|
return semanticResolveSelector(value, "selector");
|
|
326
334
|
}
|
|
327
335
|
function parseSemanticWaitOptions(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|