peachsim 0.1.527
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/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// detox-compatible expectations for sootsim
|
|
2
|
+
// expect(element).toExist(), .toBeVisible(), .toHaveText(), etc.
|
|
3
|
+
// waitFor(element).toExist().withTimeout(ms)
|
|
4
|
+
|
|
5
|
+
import { dragScrollNode } from './gestures'
|
|
6
|
+
import type { SootSimElement } from './element-types'
|
|
7
|
+
import type { Matcher } from './matchers'
|
|
8
|
+
import type { SootSimNodeInfo } from '@rnx/globals'
|
|
9
|
+
import type { Page } from 'playwright'
|
|
10
|
+
|
|
11
|
+
type FindFn = (matcher: Matcher) => Promise<any>
|
|
12
|
+
type GetPageFn = () => Page
|
|
13
|
+
const DEFAULT_VISIBILITY_THRESHOLD = 0.75
|
|
14
|
+
|
|
15
|
+
// one scroll step on the container, reporting how far it actually travelled.
|
|
16
|
+
// the caller ends the search when the container stops moving, which is how
|
|
17
|
+
// detox ends a `whileElement(...).scroll()` on a device: the native scroll
|
|
18
|
+
// throws once it reaches the end of the content.
|
|
19
|
+
async function scrollMatcherElement(
|
|
20
|
+
page: Page,
|
|
21
|
+
findNode: FindFn,
|
|
22
|
+
matcher: Matcher,
|
|
23
|
+
pixels: number,
|
|
24
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
25
|
+
startPositionX?: number,
|
|
26
|
+
startPositionY?: number,
|
|
27
|
+
): Promise<number> {
|
|
28
|
+
const node = await findNode(matcher)
|
|
29
|
+
if (!node) {
|
|
30
|
+
throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`)
|
|
31
|
+
}
|
|
32
|
+
const before = scrollOffsetOf(node)
|
|
33
|
+
await dragScrollNode(page, node, pixels, direction, startPositionX, startPositionY)
|
|
34
|
+
const after = scrollOffsetOf(await findNode(matcher))
|
|
35
|
+
return Math.abs(after.x - before.x) + Math.abs(after.y - before.y)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scrollOffsetOf(node: any): { x: number; y: number } {
|
|
39
|
+
return { x: node?.scrollOffsetX ?? 0, y: node?.scrollOffsetY ?? 0 }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// the visible frame is the largest uncovered rectangle used for tapping.
|
|
43
|
+
// visibility includes every exposed region around overlays, not just that rectangle.
|
|
44
|
+
function visibleFraction(node: SootSimNodeInfo | null | undefined): number {
|
|
45
|
+
const totalArea = (node?.layout?.width ?? 0) * (node?.layout?.height ?? 0)
|
|
46
|
+
const frame = node?.visibleFrame
|
|
47
|
+
if (totalArea <= 0 || typeof frame?.area !== 'number') return 0
|
|
48
|
+
return frame.area / totalArea
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isNodeVisible(node: SootSimNodeInfo | null | undefined): boolean {
|
|
52
|
+
return visibleFraction(node) >= DEFAULT_VISIBILITY_THRESHOLD
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// the engine tags each rendered node with its id, so DOM focus is the source of
|
|
56
|
+
// truth for both the `expect` and `waitFor` forms of toBeFocused.
|
|
57
|
+
async function isNodeFocused(node: any, page: Page): Promise<boolean> {
|
|
58
|
+
const nodeId =
|
|
59
|
+
typeof node?.nodeId === 'number'
|
|
60
|
+
? node.nodeId
|
|
61
|
+
: typeof node?.id === 'number'
|
|
62
|
+
? node.id
|
|
63
|
+
: null
|
|
64
|
+
if (nodeId === null) return false
|
|
65
|
+
return page.evaluate((id) => {
|
|
66
|
+
const active = document.activeElement
|
|
67
|
+
return active?.getAttribute('data-sootsim-id') === String(id)
|
|
68
|
+
}, nodeId)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function describeNodeVisibility(node: SootSimNodeInfo | null | undefined): string {
|
|
72
|
+
if (!node) return 'not found'
|
|
73
|
+
const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0
|
|
74
|
+
const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
layout: node.layout,
|
|
77
|
+
absolutePosition: node.absolutePosition ?? null,
|
|
78
|
+
frame: {
|
|
79
|
+
x: absX,
|
|
80
|
+
y: absY,
|
|
81
|
+
right: absX + (node.layout?.width ?? 0),
|
|
82
|
+
bottom: absY + (node.layout?.height ?? 0),
|
|
83
|
+
},
|
|
84
|
+
visibleFrame: node.visibleFrame ?? null,
|
|
85
|
+
visibleFraction: visibleFraction(node),
|
|
86
|
+
threshold: DEFAULT_VISIBILITY_THRESHOLD,
|
|
87
|
+
style: node.style ?? null,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// expect(element) returns an object with assertion methods
|
|
92
|
+
export function createExpect(findNode: FindFn, getPage: GetPageFn) {
|
|
93
|
+
return function sootsimExpect(el: SootSimElement): SootSimExpectation {
|
|
94
|
+
return new SootSimExpectation(el._matcher, findNode, getPage, false)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class SootSimExpectation {
|
|
99
|
+
private matcher: Matcher
|
|
100
|
+
private findNode: FindFn
|
|
101
|
+
private getPage: GetPageFn
|
|
102
|
+
private negated: boolean
|
|
103
|
+
|
|
104
|
+
constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn, negated: boolean) {
|
|
105
|
+
this.matcher = matcher
|
|
106
|
+
this.findNode = findNode
|
|
107
|
+
this.getPage = getPage
|
|
108
|
+
this.negated = negated
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get not(): SootSimExpectation {
|
|
112
|
+
return new SootSimExpectation(
|
|
113
|
+
this.matcher,
|
|
114
|
+
this.findNode,
|
|
115
|
+
this.getPage,
|
|
116
|
+
!this.negated,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async toExist(): Promise<void> {
|
|
121
|
+
const node = await this.findNode(this.matcher)
|
|
122
|
+
const exists = node !== null && node !== undefined
|
|
123
|
+
if (this.negated) {
|
|
124
|
+
if (exists) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (!exists) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async toBeVisible(): Promise<void> {
|
|
139
|
+
const node = await this.findNode(this.matcher)
|
|
140
|
+
const visible = isNodeVisible(node)
|
|
141
|
+
if (this.negated) {
|
|
142
|
+
if (visible) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
if (!visible) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`expected element to be visible but it is ${describeNodeVisibility(node)}: ${JSON.stringify(this.matcher)}`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async toBeNotVisible(): Promise<void> {
|
|
157
|
+
const node = await this.findNode(this.matcher)
|
|
158
|
+
const visible = isNodeVisible(node)
|
|
159
|
+
if (visible) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async toHaveText(expectedText: string): Promise<void> {
|
|
167
|
+
const node = await this.findNode(this.matcher)
|
|
168
|
+
if (!node) {
|
|
169
|
+
throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`)
|
|
170
|
+
}
|
|
171
|
+
const actualText = node.text || ''
|
|
172
|
+
const matches = actualText === expectedText
|
|
173
|
+
|
|
174
|
+
if (this.negated) {
|
|
175
|
+
if (matches) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
if (!matches) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async toHaveId(expectedId: string): Promise<void> {
|
|
190
|
+
const node = await this.findNode(this.matcher)
|
|
191
|
+
if (!node) {
|
|
192
|
+
throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`)
|
|
193
|
+
}
|
|
194
|
+
const hasId = node.testID === expectedId || node.id === expectedId
|
|
195
|
+
if (this.negated) {
|
|
196
|
+
if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`)
|
|
197
|
+
} else {
|
|
198
|
+
if (!hasId)
|
|
199
|
+
throw new Error(
|
|
200
|
+
`expected element to have id "${expectedId}" but got "${node.testID || node.id}"`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async toHaveLabel(expectedLabel: string): Promise<void> {
|
|
206
|
+
const node = await this.findNode(this.matcher)
|
|
207
|
+
if (!node) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
// check explicit accessibilityLabel, then derived text content
|
|
213
|
+
const actual = node.accessibilityLabel || node.text || ''
|
|
214
|
+
const matches = actual === expectedLabel
|
|
215
|
+
if (this.negated) {
|
|
216
|
+
if (matches)
|
|
217
|
+
throw new Error(`expected label NOT to be "${expectedLabel}" but it was`)
|
|
218
|
+
} else {
|
|
219
|
+
if (!matches)
|
|
220
|
+
throw new Error(
|
|
221
|
+
`expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async toHaveAccessibilityRole(expectedRole: string): Promise<void> {
|
|
227
|
+
const node = await this.findNode(this.matcher)
|
|
228
|
+
if (!node) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
const actual = node.accessibilityRole || ''
|
|
234
|
+
const matches = actual === expectedRole
|
|
235
|
+
if (this.negated) {
|
|
236
|
+
if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`)
|
|
237
|
+
} else {
|
|
238
|
+
if (!matches)
|
|
239
|
+
throw new Error(
|
|
240
|
+
`expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async toBeEnabled(): Promise<void> {
|
|
246
|
+
const node = await this.findNode(this.matcher)
|
|
247
|
+
if (!node) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
const disabled = node.accessibilityState?.disabled ?? false
|
|
253
|
+
if (this.negated) {
|
|
254
|
+
if (!disabled) throw new Error(`expected element to be disabled but it is enabled`)
|
|
255
|
+
} else {
|
|
256
|
+
if (disabled) throw new Error(`expected element to be enabled but it is disabled`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async toBeFocused(): Promise<void> {
|
|
261
|
+
const node = await this.findNode(this.matcher)
|
|
262
|
+
if (!node) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`element not found for toBeFocused: ${JSON.stringify(this.matcher)}`,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
const focused = await isNodeFocused(node, this.getPage())
|
|
268
|
+
|
|
269
|
+
if (this.negated) {
|
|
270
|
+
if (focused) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`expected element NOT to be focused: ${JSON.stringify(this.matcher)}`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
} else if (!focused) {
|
|
276
|
+
throw new Error(`expected element to be focused: ${JSON.stringify(this.matcher)}`)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async toHaveValue(value: string): Promise<void> {
|
|
281
|
+
const node = await this.findNode(this.matcher)
|
|
282
|
+
if (!node) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`element not found for toHaveValue: ${JSON.stringify(this.matcher)}`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
// value could be in text or props
|
|
288
|
+
const actual = node.text || ''
|
|
289
|
+
if (this.negated) {
|
|
290
|
+
if (actual.includes(value)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`expected element NOT to have value "${value}" but got "${actual}"`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
if (!actual.includes(value)) {
|
|
297
|
+
throw new Error(`expected element to have value "${value}" but got "${actual}"`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async toHaveSliderPosition(
|
|
303
|
+
normalizedPosition: number,
|
|
304
|
+
tolerance?: number,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
// stub for slider tests -- sootsim doesn't have native sliders yet
|
|
307
|
+
await this.toExist()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async toHaveToggleValue(value: boolean): Promise<void> {
|
|
311
|
+
await this.toExist()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// waitFor(element) returns a chainable object that polls until the assertion passes
|
|
316
|
+
export function createWaitFor(findNode: FindFn, getPage: GetPageFn) {
|
|
317
|
+
return function sootsimWaitFor(el: SootSimElement): SootSimWaitForChain {
|
|
318
|
+
return new SootSimWaitForChain(el._matcher, findNode, getPage)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
class SootSimWaitForChain {
|
|
323
|
+
private matcher: Matcher
|
|
324
|
+
private findNode: FindFn
|
|
325
|
+
private getPage: GetPageFn
|
|
326
|
+
private assertionFn: (() => Promise<void>) | null = null
|
|
327
|
+
private _negated = false
|
|
328
|
+
|
|
329
|
+
constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn) {
|
|
330
|
+
this.matcher = matcher
|
|
331
|
+
this.findNode = findNode
|
|
332
|
+
this.getPage = getPage
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
get not(): SootSimWaitForChain {
|
|
336
|
+
this._negated = true
|
|
337
|
+
return this
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
toExist(): SootSimWaitForAction {
|
|
341
|
+
this.assertionFn = async () => {
|
|
342
|
+
const node = await this.findNode(this.matcher)
|
|
343
|
+
const exists = node !== null && node !== undefined
|
|
344
|
+
if (this._negated ? exists : !exists) {
|
|
345
|
+
throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
toBeVisible(): SootSimWaitForAction {
|
|
352
|
+
this.assertionFn = async () => {
|
|
353
|
+
const node = await this.findNode(this.matcher)
|
|
354
|
+
const visible = isNodeVisible(node)
|
|
355
|
+
if (this._negated ? visible : !visible) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`waitFor toBeVisible failed (${describeNodeVisibility(node)}): ${JSON.stringify(this.matcher)}`,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
toBeNotVisible(): SootSimWaitForAction {
|
|
365
|
+
this.assertionFn = async () => {
|
|
366
|
+
const node = await this.findNode(this.matcher)
|
|
367
|
+
const visible = isNodeVisible(node)
|
|
368
|
+
if (visible) {
|
|
369
|
+
throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
toHaveText(expectedText: string): SootSimWaitForAction {
|
|
376
|
+
this.assertionFn = async () => {
|
|
377
|
+
const node = await this.findNode(this.matcher)
|
|
378
|
+
if (!node)
|
|
379
|
+
throw new Error(
|
|
380
|
+
`waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`,
|
|
381
|
+
)
|
|
382
|
+
const actual = node.text || ''
|
|
383
|
+
const matches = actual === expectedText
|
|
384
|
+
if (this._negated ? matches : !matches) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`,
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
toHaveValue(value: string): SootSimWaitForAction {
|
|
394
|
+
this.assertionFn = async () => {
|
|
395
|
+
const node = await this.findNode(this.matcher)
|
|
396
|
+
if (!node) throw new Error(`waitFor toHaveValue: element not found`)
|
|
397
|
+
const actual = node.text || ''
|
|
398
|
+
if (!actual.includes(value)) {
|
|
399
|
+
throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
toBeFocused(): SootSimWaitForAction {
|
|
406
|
+
this.assertionFn = async () => {
|
|
407
|
+
const node = await this.findNode(this.matcher)
|
|
408
|
+
if (!node)
|
|
409
|
+
throw new Error(
|
|
410
|
+
`waitFor toBeFocused: element not found: ${JSON.stringify(this.matcher)}`,
|
|
411
|
+
)
|
|
412
|
+
const focused = await isNodeFocused(node, this.getPage())
|
|
413
|
+
if (this._negated ? focused : !focused) {
|
|
414
|
+
throw new Error(`waitFor toBeFocused failed: ${JSON.stringify(this.matcher)}`)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return new SootSimWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
class SootSimWaitForAction {
|
|
422
|
+
private assertionFn: () => Promise<void>
|
|
423
|
+
private findNode: FindFn
|
|
424
|
+
private getPage: GetPageFn
|
|
425
|
+
|
|
426
|
+
constructor(assertionFn: () => Promise<void>, findNode: FindFn, getPage: GetPageFn) {
|
|
427
|
+
this.assertionFn = assertionFn
|
|
428
|
+
this.findNode = findNode
|
|
429
|
+
this.getPage = getPage
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async withTimeout(ms: number): Promise<void> {
|
|
433
|
+
const start = Date.now()
|
|
434
|
+
const pollInterval = 100
|
|
435
|
+
let lastError: Error | null = null
|
|
436
|
+
|
|
437
|
+
while (Date.now() - start < ms) {
|
|
438
|
+
try {
|
|
439
|
+
await this.assertionFn()
|
|
440
|
+
return // assertion passed
|
|
441
|
+
} catch (e) {
|
|
442
|
+
lastError = e as Error
|
|
443
|
+
await new Promise((r) => setTimeout(r, pollInterval))
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
throw lastError || new Error(`waitFor timed out after ${ms}ms`)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// if no withTimeout is called, just run the assertion once
|
|
451
|
+
async then(resolve: (v: void) => void, reject: (e: any) => void): Promise<void> {
|
|
452
|
+
try {
|
|
453
|
+
// default 5 second timeout
|
|
454
|
+
await this.withTimeout(5000)
|
|
455
|
+
resolve(undefined)
|
|
456
|
+
} catch (e) {
|
|
457
|
+
reject(e)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
whileElement(matcher: Matcher): SootSimWaitForScrollAction {
|
|
462
|
+
return new SootSimWaitForScrollAction(
|
|
463
|
+
this.assertionFn,
|
|
464
|
+
this.findNode,
|
|
465
|
+
this.getPage,
|
|
466
|
+
matcher,
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
class SootSimWaitForScrollAction {
|
|
472
|
+
private assertionFn: () => Promise<void>
|
|
473
|
+
private findNode: FindFn
|
|
474
|
+
private getPage: GetPageFn
|
|
475
|
+
private matcher: Matcher
|
|
476
|
+
|
|
477
|
+
constructor(
|
|
478
|
+
assertionFn: () => Promise<void>,
|
|
479
|
+
findNode: FindFn,
|
|
480
|
+
getPage: GetPageFn,
|
|
481
|
+
matcher: Matcher,
|
|
482
|
+
) {
|
|
483
|
+
this.assertionFn = assertionFn
|
|
484
|
+
this.findNode = findNode
|
|
485
|
+
this.getPage = getPage
|
|
486
|
+
this.matcher = matcher
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// detox scrolls the container until the expectation passes or the container
|
|
490
|
+
// can no longer move. bounding this by wall clock instead made the result
|
|
491
|
+
// depend on how fast the host dispatches pointer events: the same fixture and
|
|
492
|
+
// the same assertion pass on an idle machine and fail on a loaded one,
|
|
493
|
+
// because each 200px drag is a real pointer stream and fewer of them fit in
|
|
494
|
+
// the budget. end on scroll exhaustion, which is a property of the content.
|
|
495
|
+
async scroll(
|
|
496
|
+
pixels: number,
|
|
497
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
498
|
+
startPositionX?: number,
|
|
499
|
+
startPositionY?: number,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
const page = this.getPage()
|
|
502
|
+
let lastError: Error | null = null
|
|
503
|
+
let stalledSteps = 0
|
|
504
|
+
|
|
505
|
+
// two consecutive steps, so one settling frame that reports the same offset
|
|
506
|
+
// does not read as the end of the content.
|
|
507
|
+
while (stalledSteps < 2) {
|
|
508
|
+
try {
|
|
509
|
+
await this.assertionFn()
|
|
510
|
+
return
|
|
511
|
+
} catch (error) {
|
|
512
|
+
lastError = error as Error
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const travelled = await scrollMatcherElement(
|
|
516
|
+
page,
|
|
517
|
+
this.findNode,
|
|
518
|
+
this.matcher,
|
|
519
|
+
pixels,
|
|
520
|
+
direction,
|
|
521
|
+
startPositionX,
|
|
522
|
+
startPositionY,
|
|
523
|
+
)
|
|
524
|
+
stalledSteps = travelled < 0.5 ? stalledSteps + 1 : 0
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
throw (
|
|
528
|
+
lastError ||
|
|
529
|
+
new Error(
|
|
530
|
+
`waitFor whileElement(...).scroll() reached the end of ${JSON.stringify(this.matcher)}`,
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
}
|