agent-device 0.19.3 → 0.20.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/README.md +61 -70
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk.sha256 +1 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.manifest.json +10 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk.sha256 +1 -0
- package/{android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.manifest.json → android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.manifest.json} +6 -6
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +2 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +5 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +5 -10
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +74 -174
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +113 -19
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift +82 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +272 -118
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +2 -2
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +134 -101
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +56 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +73 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +44 -24
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +2 -2
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +2 -107
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +126 -11
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +51 -8
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +24 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +1 -1
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +43 -35
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +14 -0
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +3 -0
- package/dist/src/android-adb.d.ts +1 -1
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-method-overlays.js +1 -0
- package/dist/src/android-input-ownership.js +1 -6
- package/dist/src/android-system-chrome.js +1 -0
- package/dist/src/android.js +1 -1
- package/dist/src/app-lifecycle.js +5 -5
- package/dist/src/app-log-request-scope.js +1 -0
- package/dist/src/app-resolution.js +1 -0
- package/dist/src/apple-multitouch-support.js +1 -0
- package/dist/src/apps.js +5 -5
- package/dist/src/boot-diagnostics.js +2 -0
- package/dist/src/cli-config.js +1 -0
- package/dist/src/cli-help.js +61 -27
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +15 -11
- package/dist/src/command-schema.js +3 -3
- package/dist/src/config.js +1 -0
- package/dist/src/context.js +1 -0
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -0
- package/dist/src/debug-symbols.js +1 -1
- package/dist/src/device-input-state.js +1 -1
- package/dist/src/devices2.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/helper.js +1 -1
- package/dist/src/index.d.ts +412 -293
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +4 -3
- package/dist/src/install-artifact.js +1 -0
- package/dist/src/install-source.d.ts +5 -1
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction.d.ts +230 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +42 -20
- package/dist/src/internal/png-worker.d.ts +22 -0
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/launch-console.js +1 -0
- package/dist/src/lease-context.js +1 -0
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +5 -0
- package/dist/src/metro.d.ts +2 -2
- package/dist/src/notifications.js +1 -1
- package/dist/src/png-worker-contract.js +1 -1
- package/dist/src/process-lock.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/register-builtins.js +1 -0
- package/dist/src/registry.js +24 -24
- package/dist/src/reporting.js +8 -7
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-disposal.js +2 -0
- package/dist/src/runner-lease.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/runtime2.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +119 -33
- package/dist/src/sdk-android-adb.js +2 -2
- package/dist/src/sdk-batch-runner.d.ts +2 -1
- package/dist/src/sdk-batch-runner.js +2 -2
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +105 -98
- package/dist/src/sdk-contracts.js +1 -1
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.d.ts +3 -3
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.d.ts +1 -1
- package/dist/src/sdk-remote-config.d.ts +9 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selector-vocabulary.js +1 -0
- package/dist/src/selectors.d.ts +12 -3
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session-target-evidence.js +1 -0
- package/dist/src/session.js +8 -10
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +5 -2
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -2
- package/dist/src/tv-remote.js +13 -12
- package/dist/src/url.js +1 -1
- package/dist/src/web.js +1 -1
- package/package.json +28 -23
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk.sha256 +0 -1
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.manifest.json +0 -10
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk.sha256 +0 -1
- package/dist/src/apple-runner-options.js +0 -1
- package/dist/src/cli-output.js +0 -1
- package/dist/src/client.js +0 -3
- package/dist/src/handler-utils.js +0 -1
- package/dist/src/output-path.js +0 -1
- package/dist/src/points.js +0 -4
- package/dist/src/provider.js +0 -1
- package/dist/src/providers.js +0 -1
- package/dist/src/record-trace-recording.js +0 -23
- package/dist/src/recording-provider.js +0 -1
- package/dist/src/selectors2.js +0 -1
- package/dist/src/simctl.js +0 -1
- package/dist/src/ttl-memo.js +0 -1
- package/dist/src/xml.js +0 -2
- /package/{macos-helper → apple/macos-helper}/Package.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/AudioProbe.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/main.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
- /package/{src/platforms/linux → linux}/atspi-dump.py +0 -0
package/README.md
CHANGED
|
@@ -13,42 +13,17 @@
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
[](https://glama.ai/mcp/servers/callstack/agent-device)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Let your coding agent verify its changes in the running app.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
`agent-device` lets coding agents inspect, control, and verify apps on iOS, Android, TV, web, macOS, and Linux. Agents can read token-efficient accessibility snapshots, find elements by ref or selector, run device actions, and save evidence for review.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Your coding agent or QA tool reads each result and chooses the next command. `agent-device` runs the command and saves evidence when asked.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
`agent-device` uses the inspect-act-verify process from Vercel's [agent-browser](https://github.com/vercel-labs/agent-browser) for mobile, TV, and desktop apps. Basic `--platform web` support runs `agent-browser` in the same session and replay system.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## Quick start
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-

|
|
29
|
-
|
|
30
|
-
## Capabilities
|
|
31
|
-
|
|
32
|
-
- **Inspect** real app UI through structured accessibility snapshots, interactive refs like `@e3`, selectors, and React Native component trees.
|
|
33
|
-
- **Interact** by opening apps, tapping, typing, scrolling, performing gestures, waiting, asserting state, handling alerts, and closing sessions.
|
|
34
|
-
- **Capture evidence** with screenshots, videos, logs, traces, network traffic, audio-level probes for browser and host-rendered simulator/emulator audio, performance samples, crash context, and React profiles.
|
|
35
|
-
- **Replay workflows** by recording `.ad` scripts for local runs, CI, repeatable e2e checks, and strict Maestro YAML export when a flow needs to run in Maestro.
|
|
36
|
-
- **Run across platforms** with iOS Simulator automation, Android Emulator automation, physical devices, tvOS, Android TV, macOS, Linux, and desktop app automation, so agents can see and feel the app they work on.
|
|
37
|
-
|
|
38
|
-
## Use Cases
|
|
39
|
-
|
|
40
|
-
- Verify mobile changes on real devices, simulators, and emulators before review or merge.
|
|
41
|
-
- Give AI coding agents a real app feedback loop while they implement features.
|
|
42
|
-
- Debug regressions with screenshots, logs, traces, network/audio evidence, and crash context.
|
|
43
|
-
- Profile performance issues with CPU/memory samples and React render profiles when needed.
|
|
44
|
-
- Turn exploratory app interactions into replayable e2e checks for CI.
|
|
45
|
-
- Use one agent workflow across native iOS, Android, Expo, Flutter, React Native, TV, and desktop apps.
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-
## Quick Start
|
|
50
|
-
|
|
51
|
-
Install the CLI:
|
|
26
|
+
Install the CLI and check setup. It requires Node.js 22.12 or newer; web automation requires Node.js 24 or newer. See [Installation](https://oss.callstack.com/agent-device/docs/installation) for target requirements.
|
|
52
27
|
|
|
53
28
|
```bash
|
|
54
29
|
npm install -g agent-device@latest
|
|
@@ -57,46 +32,58 @@ agent-device --version
|
|
|
57
32
|
agent-device help workflow
|
|
58
33
|
```
|
|
59
34
|
|
|
60
|
-
Run `agent-device doctor` yourself
|
|
61
|
-
handing the CLI to an agent. The installed CLI help is the source of truth for
|
|
62
|
-
agents. Start with `agent-device help workflow`, then follow the topic-specific
|
|
63
|
-
help when a task needs dogfooding, debugging, replay, or React Native profiling.
|
|
35
|
+
Run `agent-device doctor` yourself before handing the CLI to an agent. The installed CLI help defines current behavior. `agent-device help workflow` links to guides for debugging, replay, React Native profiling, and other tasks.
|
|
64
36
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Try the basic loop:
|
|
37
|
+
Add a contact in the built-in iOS Contacts app:
|
|
68
38
|
|
|
69
39
|
```bash
|
|
70
|
-
# Find an app.
|
|
71
|
-
agent-device apps --platform ios
|
|
72
|
-
agent-device apps --platform android
|
|
73
|
-
|
|
74
40
|
# Start a session.
|
|
75
|
-
agent-device open
|
|
41
|
+
agent-device open Contacts --platform ios
|
|
76
42
|
|
|
77
|
-
# Inspect the
|
|
43
|
+
# Inspect the screen. The example below shows the output; refs vary.
|
|
78
44
|
agent-device snapshot -i
|
|
79
|
-
# @
|
|
80
|
-
# @e2 [button] "Sign In"
|
|
81
|
-
# @e3 [text-field] "Email"
|
|
45
|
+
# @e2 [button] "Add"
|
|
82
46
|
|
|
83
|
-
#
|
|
84
|
-
agent-device
|
|
85
|
-
|
|
47
|
+
# Use the ref and wait for the UI to settle.
|
|
48
|
+
agent-device press @e2 --settle
|
|
49
|
+
# The diff includes:
|
|
50
|
+
# + @e7 [text-field] "First name"
|
|
51
|
+
|
|
52
|
+
agent-device fill @e7 "Ada" --settle
|
|
53
|
+
# The next diff shows changed values and current refs:
|
|
54
|
+
# - @e7 [text-field] "First name"
|
|
55
|
+
# + @e14 [text-field] "Ada"
|
|
56
|
+
# = @e15 [text-field] "Last name"
|
|
57
|
+
|
|
58
|
+
# Capture evidence and close the session.
|
|
59
|
+
agent-device screenshot ./contact-form.png
|
|
86
60
|
agent-device close
|
|
87
61
|
```
|
|
88
62
|
|
|
89
|
-
|
|
63
|
+
Use refs only from the latest output. Do not assume an earlier `@eN` still identifies the same element. After a command with `--settle`, use the refs in its diff. Take another snapshot only if the diff omits what you need.
|
|
64
|
+
|
|
65
|
+
Snapshots use the app's accessibility tree. Clear labels, roles, and test IDs make agent runs more reliable. Use screenshots and videos as evidence or when accessibility data is poor. Use refs and selectors for actions and assertions when you can.
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
## What agents can do
|
|
70
|
+
|
|
71
|
+
- **Inspect app state** through accessibility snapshots, refs, selectors, and React Native component trees.
|
|
72
|
+
- **Act on visible UI** by tapping or pressing elements, filling fields, scrolling, making gestures, waiting, asserting state, and handling alerts.
|
|
73
|
+
- **Diagnose failures** with screenshots, video, logs, traces, network data, performance samples, crash details, and React profiles.
|
|
74
|
+
- **Repeat workflows** by saving working steps as `.ad` scripts for local use or CI. Export strict Maestro YAML when needed.
|
|
75
|
+
|
|
76
|
+
See [Commands](https://oss.callstack.com/agent-device/docs/commands) for the commands and evidence each target supports.
|
|
90
77
|
|
|
91
|
-
|
|
78
|
+

|
|
92
79
|
|
|
93
|
-
## Next
|
|
80
|
+
## Next steps
|
|
94
81
|
|
|
95
|
-
- **Set up your agent**: run the CLI from Cursor, Codex, Claude Code, Windsurf, or another agent terminal.
|
|
96
|
-
- **Try the sample app**: clone the repo and run the bundled Expo
|
|
97
|
-
- **
|
|
82
|
+
- **Set up your agent**: run the CLI from Cursor, Codex, Claude Code, Windsurf, or another agent terminal. See [AI Agent Setup](https://oss.callstack.com/agent-device/docs/agent-setup) for skills, rules, MCP tools, and setup for each client.
|
|
83
|
+
- **Try the sample app**: clone the repo and run the bundled Expo test app. [Quick Start](https://oss.callstack.com/agent-device/docs/quick-start) covers a guided run with screenshots, replay, and performance data.
|
|
84
|
+
- **Build repeatable tests**: use [Replay & E2E](https://oss.callstack.com/agent-device/docs/replay-e2e) to repeat tests. Use [Debugging & Profiling](https://oss.callstack.com/agent-device/docs/debugging-profiling) to find bugs.
|
|
98
85
|
|
|
99
|
-
## Articles
|
|
86
|
+
## Articles and videos
|
|
100
87
|
|
|
101
88
|
### Articles
|
|
102
89
|
|
|
@@ -111,37 +98,41 @@ Snapshots come from the app's accessibility tree, so high-quality labels, roles,
|
|
|
111
98
|
- [Using agent-device in an AI coding workflow](https://youtu.be/dfVG_aNPkW4)
|
|
112
99
|
- [Cloud agents that test mobile apps on real devices](https://youtu.be/r5P0detC4bs?is=_KB6SZbLFRB1au_z)
|
|
113
100
|
|
|
114
|
-
## Where
|
|
101
|
+
## Where to run agent-device
|
|
115
102
|
|
|
116
103
|
| Path | Best for | Start with |
|
|
117
104
|
| --- | --- | --- |
|
|
118
|
-
| Local |
|
|
119
|
-
| CI/CD | Automated
|
|
120
|
-
| Cloud / remote
|
|
105
|
+
| Local | Trying commands and debugging apps on simulators, emulators, physical devices, macOS, and Linux. | Follow the Quick Start. |
|
|
106
|
+
| CI/CD | Automated pull request and merge validation with replay scripts and captured artifacts. | Try the [EAS workflow template](https://github.com/callstackincubator/eas-agent-device/blob/main/.eas/workflows/agent-qa-mobile.yml). GitHub Actions template coming soon. |
|
|
107
|
+
| Cloud / remote | Linux runners, managed devices, and remote jobs. | Use [Agent Device Cloud](https://agent-device.dev/cloud), set a remote profile with [Commands](https://oss.callstack.com/agent-device/docs/commands), or [contact Callstack](mailto:hello@callstack.com) for team QA. |
|
|
121
108
|
|
|
122
|
-
## How
|
|
109
|
+
## How it works
|
|
123
110
|
|
|
124
|
-
`agent-device`
|
|
111
|
+
`agent-device` keeps device state in sessions. It sends commands to XCTest on iOS and tvOS, ADB and the snapshot helper on Android, a local helper on macOS, and AT-SPI on Linux.
|
|
125
112
|
|
|
126
|
-
Node
|
|
113
|
+
Node.js apps can use the typed client or public subpaths. `agent-device/android-adb` provides the Android ADB provider interface, helpers for logcat, the clipboard, the keyboard, and apps, and port reverse management.
|
|
127
114
|
|
|
128
115
|
## FAQ
|
|
129
116
|
|
|
130
117
|
### What is agent-device?
|
|
131
118
|
|
|
132
|
-
`agent-device` is a
|
|
119
|
+
`agent-device` is a command-line tool that lets coding agents inspect, control, and verify apps and save evidence for review. It supports iOS, Android, TV, web, macOS, and Linux.
|
|
133
120
|
|
|
134
121
|
### Does it work with React Native, Expo, Flutter, and native apps?
|
|
135
122
|
|
|
136
|
-
Yes. `agent-device`
|
|
123
|
+
Yes. `agent-device` supports native iOS and Android apps, plus React Native, Expo, and Flutter apps on supported targets. The commands and evidence vary by target.
|
|
137
124
|
|
|
138
125
|
### How is it different from Appium, Detox, or Maestro?
|
|
139
126
|
|
|
140
|
-
Appium, Detox, and Maestro
|
|
127
|
+
With `agent-device`, an agent reads app state and chooses each command at run time. Teams use Appium, Detox, and Maestro to write and maintain test suites. `agent-device` can complement them by saving its runs as `.ad` scripts or exporting them as strict Maestro YAML.
|
|
128
|
+
|
|
129
|
+
### Can agent-device run in CI?
|
|
130
|
+
|
|
131
|
+
Yes. Record a run as an `.ad` script, replay it locally or in CI, and save screenshots, logs, and other artifacts for review. See [Replay & E2E](https://oss.callstack.com/agent-device/docs/replay-e2e) or start with the [EAS workflow template](https://github.com/callstackincubator/eas-agent-device/blob/main/.eas/workflows/agent-qa-mobile.yml).
|
|
141
132
|
|
|
142
|
-
##
|
|
133
|
+
## Who uses agent-device?
|
|
143
134
|
|
|
144
|
-
|
|
135
|
+
Teams and developers at Callstack, JPMorgan Chase, [Expensify](https://www.callstack.com/blog/how-expensify-uses-agent-device-for-mobile-bug-evidence-and-profiling), [Shopify](https://x.com/mustafa01ali/status/2036577353178943826), Kindred, [Total Wine & More](https://www.callstack.com/podcasts/how-ai-is-changing-react-native-development-and-testing), [LegendList](https://x.com/jmeistrich/status/2036398735698305178), HerLyfe, App & Flow, and others use `agent-device`.
|
|
145
136
|
|
|
146
137
|
## Documentation
|
|
147
138
|
|
|
@@ -154,4 +145,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
154
145
|
|
|
155
146
|
## Made at Callstack
|
|
156
147
|
|
|
157
|
-
`agent-device` is open source
|
|
148
|
+
`agent-device` is open source under the MIT license. Visit [agent-device.dev](https://agent-device.dev/) or [contact Callstack](mailto:hello@callstack.com).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
315907205b4d573cbfe4dfa4bcb7bafd360d9f6d93427f255574d7f02ea6656e agent-device-android-ime-helper-0.20.0.apk
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "android-ime-helper",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"assetName": "agent-device-android-ime-helper-0.20.0.apk",
|
|
5
|
+
"sha256": "315907205b4d573cbfe4dfa4bcb7bafd360d9f6d93427f255574d7f02ea6656e",
|
|
6
|
+
"packageName": "com.callstack.agentdevice.imehelper",
|
|
7
|
+
"versionCode": 20000,
|
|
8
|
+
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
|
+
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d635fe2e577ece9d5924719f907232693231644d69ba08b7d21d7bea04baa652 agent-device-android-snapshot-helper-0.20.0.apk
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-snapshot-helper",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"releaseTag": "v0.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"releaseTag": "v0.20.0",
|
|
5
|
+
"assetName": "agent-device-android-snapshot-helper-0.20.0.apk",
|
|
6
6
|
"apkUrl": null,
|
|
7
|
-
"sha256": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.
|
|
7
|
+
"sha256": "d635fe2e577ece9d5924719f907232693231644d69ba08b7d21d7bea04baa652",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.20.0.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 20000,
|
|
11
11
|
"instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
|
|
12
12
|
"minSdk": 23,
|
|
13
13
|
"targetSdk": 36,
|
|
@@ -164,6 +164,11 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
164
164
|
return send(target, selector);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
+ (NSInteger)processIdentifierForApplication:(XCUIApplication *)application
|
|
168
|
+
{
|
|
169
|
+
return [self integerFrom:application selectorName:@"processID"];
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
+ (id)accessibilityApplicationForApplication:(XCUIApplication *)application axClient:(id)axClient
|
|
168
173
|
{
|
|
169
174
|
NSInteger targetProcessID = [self integerFrom:application selectorName:@"processID"];
|
|
@@ -4,16 +4,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
4
4
|
|
|
5
5
|
@interface RunnerSynthesizedGesture : NSObject
|
|
6
6
|
|
|
7
|
-
+ (NSString * _Nullable)synthesizeTransformWithApplication:(id)application
|
|
8
|
-
x:(double)x
|
|
9
|
-
y:(double)y
|
|
10
|
-
dx:(double)dx
|
|
11
|
-
dy:(double)dy
|
|
12
|
-
scale:(double)scale
|
|
13
|
-
degrees:(double)degrees
|
|
14
|
-
radius:(double)radius
|
|
15
|
-
durationMs:(double)durationMs;
|
|
16
|
-
|
|
17
7
|
+ (NSString * _Nullable)synthesizeSwipeWithApplication:(id)application
|
|
18
8
|
x:(double)x
|
|
19
9
|
y:(double)y
|
|
@@ -32,6 +22,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
32
22
|
x:(double)x
|
|
33
23
|
y:(double)y;
|
|
34
24
|
|
|
25
|
+
// Each pointer is an ordered array of { x, y, offsetMs } samples. The first sample
|
|
26
|
+
// starts contact; subsequent samples move it; all pointers lift at their final offset.
|
|
27
|
+
+ (NSString * _Nullable)synthesizeGestureWithApplication:(id)application
|
|
28
|
+
pointerSamples:(NSArray<NSArray<NSDictionary<NSString *, NSNumber *> *> *> *)pointerSamples;
|
|
29
|
+
|
|
35
30
|
// UIInterfaceOrientation of the app (1 portrait, 2 upsideDown, 3 landscapeRight,
|
|
36
31
|
// 4 landscapeLeft), or 0 if unreadable.
|
|
37
32
|
+ (NSInteger)interfaceOrientationForApplication:(id)application;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#import "RunnerSynthesizedGesture.h"
|
|
2
2
|
|
|
3
3
|
#import <CoreGraphics/CoreGraphics.h>
|
|
4
|
-
#import <math.h>
|
|
5
4
|
#import <objc/message.h>
|
|
6
5
|
|
|
7
6
|
typedef NSInteger (*RunnerMsgSendInteger)(id, SEL);
|
|
@@ -41,19 +40,6 @@ static NSString * _Nullable RunnerResolveXCTestEventBridge(
|
|
|
41
40
|
static NSString * _Nullable RunnerRequireClass(Class cls, NSString *className);
|
|
42
41
|
static NSString * _Nullable RunnerRequireSelector(Class cls, SEL selector, NSString *selectorName);
|
|
43
42
|
static NSString * _Nullable RunnerRequireApplicationSelector(id application, SEL selector, NSString *selectorName);
|
|
44
|
-
static id RunnerPointerPath(
|
|
45
|
-
const RunnerXCTestEventBridge *bridge,
|
|
46
|
-
CGPoint start,
|
|
47
|
-
double x,
|
|
48
|
-
double y,
|
|
49
|
-
double dx,
|
|
50
|
-
double dy,
|
|
51
|
-
double scale,
|
|
52
|
-
double degrees,
|
|
53
|
-
double radius,
|
|
54
|
-
double durationMs,
|
|
55
|
-
double side
|
|
56
|
-
);
|
|
57
43
|
static id RunnerSwipePointerPath(
|
|
58
44
|
const RunnerXCTestEventBridge *bridge,
|
|
59
45
|
CGPoint start,
|
|
@@ -74,54 +60,16 @@ static NSString * _Nullable RunnerTrySynthesizeDrag(
|
|
|
74
60
|
NSString *recordName,
|
|
75
61
|
RunnerDragPointerPathFactory pathFactory
|
|
76
62
|
);
|
|
77
|
-
// XCTest's
|
|
78
|
-
//
|
|
79
|
-
// nested UIKit gesture recognizers.
|
|
63
|
+
// XCTest's proven swipe profile reaches the endpoint in 100 ms, then holds for the planned
|
|
64
|
+
// fling duration. Fast movement is what lets UIKit distinguish a fling from a timed pan.
|
|
80
65
|
static const NSTimeInterval RunnerSwipeMovementDurationSeconds = 0.1;
|
|
81
66
|
static id RunnerTapPointerPath(
|
|
82
67
|
const RunnerXCTestEventBridge *bridge,
|
|
83
68
|
CGPoint point
|
|
84
69
|
);
|
|
85
|
-
static CGPoint RunnerPointerPointAt(
|
|
86
|
-
double x,
|
|
87
|
-
double y,
|
|
88
|
-
double dx,
|
|
89
|
-
double dy,
|
|
90
|
-
double scale,
|
|
91
|
-
double degrees,
|
|
92
|
-
double baseRadius,
|
|
93
|
-
double t,
|
|
94
|
-
double side
|
|
95
|
-
);
|
|
96
70
|
|
|
97
71
|
@implementation RunnerSynthesizedGesture
|
|
98
72
|
|
|
99
|
-
+ (NSString * _Nullable)synthesizeTransformWithApplication:(id)application
|
|
100
|
-
x:(double)x
|
|
101
|
-
y:(double)y
|
|
102
|
-
dx:(double)dx
|
|
103
|
-
dy:(double)dy
|
|
104
|
-
scale:(double)scale
|
|
105
|
-
degrees:(double)degrees
|
|
106
|
-
radius:(double)radius
|
|
107
|
-
durationMs:(double)durationMs {
|
|
108
|
-
@try {
|
|
109
|
-
return [self trySynthesizeTransformWithApplication:application
|
|
110
|
-
x:x
|
|
111
|
-
y:y
|
|
112
|
-
dx:dx
|
|
113
|
-
dy:dy
|
|
114
|
-
scale:scale
|
|
115
|
-
degrees:degrees
|
|
116
|
-
radius:radius
|
|
117
|
-
durationMs:durationMs];
|
|
118
|
-
} @catch (NSException *exception) {
|
|
119
|
-
NSString *name = exception.name ?: @"NSException";
|
|
120
|
-
NSString *reason = exception.reason ?: @"private XCTest event synthesis failed";
|
|
121
|
-
return [NSString stringWithFormat:@"%@: %@", name, reason];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
73
|
+ (NSString * _Nullable)synthesizeSwipeWithApplication:(id)application
|
|
126
74
|
x:(double)x
|
|
127
75
|
y:(double)y
|
|
@@ -178,76 +126,84 @@ static CGPoint RunnerPointerPointAt(
|
|
|
178
126
|
}
|
|
179
127
|
}
|
|
180
128
|
|
|
181
|
-
+ (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
degrees:(double)degrees
|
|
196
|
-
radius:(double)radius
|
|
197
|
-
durationMs:(double)durationMs {
|
|
198
|
-
RunnerXCTestEventBridge bridge;
|
|
199
|
-
NSString *missing = RunnerResolveXCTestEventBridge(application, &bridge);
|
|
200
|
-
if (missing != nil) {
|
|
201
|
-
return missing;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
NSInteger interfaceOrientation =
|
|
205
|
-
((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector);
|
|
206
|
-
NSInteger targetProcessID = ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector);
|
|
207
|
-
if (targetProcessID <= 0) {
|
|
208
|
-
return @"private XCTest event synthesis unavailable: could not resolve target process ID";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
|
|
212
|
-
[bridge.recordClass alloc],
|
|
213
|
-
bridge.initRecordSelector,
|
|
214
|
-
@"agent-device-transform",
|
|
215
|
-
interfaceOrientation
|
|
216
|
-
);
|
|
217
|
-
if (record == nil) {
|
|
218
|
-
return @"private XCTest event synthesis failed: could not create event record";
|
|
219
|
-
}
|
|
220
|
-
((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector, targetProcessID);
|
|
129
|
+
+ (NSString * _Nullable)synthesizeGestureWithApplication:(id)application
|
|
130
|
+
pointerSamples:(NSArray<NSArray<NSDictionary<NSString *, NSNumber *> *> *> *)pointerSamples {
|
|
131
|
+
@try {
|
|
132
|
+
RunnerXCTestEventBridge bridge;
|
|
133
|
+
NSString *missing = RunnerResolveXCTestEventBridge(application, &bridge);
|
|
134
|
+
if (missing != nil) return missing;
|
|
135
|
+
|
|
136
|
+
NSInteger interfaceOrientation =
|
|
137
|
+
((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector);
|
|
138
|
+
NSInteger targetProcessID =
|
|
139
|
+
((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector);
|
|
140
|
+
if (targetProcessID <= 0) {
|
|
141
|
+
return @"private XCTest event synthesis unavailable: could not resolve target process ID";
|
|
142
|
+
}
|
|
221
143
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
RunnerPointerPointAt(x, y, dx, dy, scale, degrees, radius, 0.0, side),
|
|
228
|
-
x,
|
|
229
|
-
y,
|
|
230
|
-
dx,
|
|
231
|
-
dy,
|
|
232
|
-
scale,
|
|
233
|
-
degrees,
|
|
234
|
-
radius,
|
|
235
|
-
durationMs,
|
|
236
|
-
side
|
|
144
|
+
id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
|
|
145
|
+
[bridge.recordClass alloc],
|
|
146
|
+
bridge.initRecordSelector,
|
|
147
|
+
@"agent-device-gesture-plan",
|
|
148
|
+
interfaceOrientation
|
|
237
149
|
);
|
|
238
|
-
if (
|
|
239
|
-
return @"private XCTest event synthesis failed: could not create
|
|
150
|
+
if (record == nil) {
|
|
151
|
+
return @"private XCTest event synthesis failed: could not create event record";
|
|
152
|
+
}
|
|
153
|
+
((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector, targetProcessID);
|
|
154
|
+
|
|
155
|
+
for (NSArray<NSDictionary<NSString *, NSNumber *> *> *samples in pointerSamples) {
|
|
156
|
+
NSDictionary<NSString *, NSNumber *> *first = samples.firstObject;
|
|
157
|
+
if (first == nil) return @"private XCTest event synthesis failed: empty pointer path";
|
|
158
|
+
CGPoint start = CGPointMake(first[@"x"].doubleValue, first[@"y"].doubleValue);
|
|
159
|
+
id path = ((RunnerMsgSendInitPath)objc_msgSend)(
|
|
160
|
+
[bridge.pathClass alloc],
|
|
161
|
+
bridge.initPathSelector,
|
|
162
|
+
start,
|
|
163
|
+
first[@"offsetMs"].doubleValue / 1000.0
|
|
164
|
+
);
|
|
165
|
+
if (path == nil) {
|
|
166
|
+
return @"private XCTest event synthesis failed: could not create pointer path";
|
|
167
|
+
}
|
|
168
|
+
for (NSUInteger index = 1; index < samples.count; index += 1) {
|
|
169
|
+
NSDictionary<NSString *, NSNumber *> *sample = samples[index];
|
|
170
|
+
CGPoint point = CGPointMake(sample[@"x"].doubleValue, sample[@"y"].doubleValue);
|
|
171
|
+
((RunnerMsgSendPathMove)objc_msgSend)(
|
|
172
|
+
path,
|
|
173
|
+
bridge.moveSelector,
|
|
174
|
+
point,
|
|
175
|
+
sample[@"offsetMs"].doubleValue / 1000.0
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
NSDictionary<NSString *, NSNumber *> *last = samples.lastObject;
|
|
179
|
+
((RunnerMsgSendPathOffset)objc_msgSend)(
|
|
180
|
+
path,
|
|
181
|
+
bridge.liftSelector,
|
|
182
|
+
last[@"offsetMs"].doubleValue / 1000.0
|
|
183
|
+
);
|
|
184
|
+
((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector, path);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
NSError *error = nil;
|
|
188
|
+
BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector, &error);
|
|
189
|
+
if (!ok) {
|
|
190
|
+
NSString *detail = error.localizedDescription ?: @"synthesizeWithError returned false";
|
|
191
|
+
return [NSString stringWithFormat:@"private XCTest event synthesis failed: %@", detail];
|
|
240
192
|
}
|
|
241
|
-
|
|
193
|
+
return nil;
|
|
194
|
+
} @catch (NSException *exception) {
|
|
195
|
+
NSString *name = exception.name ?: @"NSException";
|
|
196
|
+
NSString *reason = exception.reason ?: @"private XCTest event synthesis failed";
|
|
197
|
+
return [NSString stringWithFormat:@"%@: %@", name, reason];
|
|
242
198
|
}
|
|
199
|
+
}
|
|
243
200
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!
|
|
247
|
-
|
|
248
|
-
return [NSString stringWithFormat:@"private XCTest event synthesis failed: %@", detail];
|
|
201
|
+
+ (NSInteger)interfaceOrientationForApplication:(id)application {
|
|
202
|
+
SEL selector = NSSelectorFromString(@"interfaceOrientation");
|
|
203
|
+
if (![application respondsToSelector:selector]) {
|
|
204
|
+
return 0; // UIInterfaceOrientationUnknown
|
|
249
205
|
}
|
|
250
|
-
return
|
|
206
|
+
return ((RunnerMsgSendInteger)objc_msgSend)(application, selector);
|
|
251
207
|
}
|
|
252
208
|
|
|
253
209
|
static NSString * _Nullable RunnerTrySynthesizeDrag(
|
|
@@ -426,37 +382,6 @@ static NSString * _Nullable RunnerRequireApplicationSelector(
|
|
|
426
382
|
return nil;
|
|
427
383
|
}
|
|
428
384
|
|
|
429
|
-
static id RunnerPointerPath(
|
|
430
|
-
const RunnerXCTestEventBridge *bridge,
|
|
431
|
-
CGPoint start,
|
|
432
|
-
double x,
|
|
433
|
-
double y,
|
|
434
|
-
double dx,
|
|
435
|
-
double dy,
|
|
436
|
-
double scale,
|
|
437
|
-
double degrees,
|
|
438
|
-
double radius,
|
|
439
|
-
double durationMs,
|
|
440
|
-
double side
|
|
441
|
-
) {
|
|
442
|
-
id path =
|
|
443
|
-
((RunnerMsgSendInitPath)objc_msgSend)([bridge->pathClass alloc], bridge->initPathSelector, start, 0.0);
|
|
444
|
-
if (path == nil) {
|
|
445
|
-
return nil;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
int frameCount = MAX(3, (int)(durationMs / 16.0));
|
|
449
|
-
NSTimeInterval durationSeconds = durationMs / 1000.0;
|
|
450
|
-
for (int index = 1; index <= frameCount; index += 1) {
|
|
451
|
-
double t = (double)index / (double)frameCount;
|
|
452
|
-
CGPoint point = RunnerPointerPointAt(x, y, dx, dy, scale, degrees, radius, t, side);
|
|
453
|
-
NSTimeInterval offset = durationSeconds * t;
|
|
454
|
-
((RunnerMsgSendPathMove)objc_msgSend)(path, bridge->moveSelector, point, offset);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
((RunnerMsgSendPathOffset)objc_msgSend)(path, bridge->liftSelector, durationSeconds);
|
|
458
|
-
return path;
|
|
459
|
-
}
|
|
460
385
|
|
|
461
386
|
static id RunnerSwipePointerPath(
|
|
462
387
|
const RunnerXCTestEventBridge *bridge,
|
|
@@ -477,7 +402,6 @@ static id RunnerSwipePointerPath(
|
|
|
477
402
|
end,
|
|
478
403
|
RunnerSwipeMovementDurationSeconds
|
|
479
404
|
);
|
|
480
|
-
|
|
481
405
|
((RunnerMsgSendPathOffset)objc_msgSend)(
|
|
482
406
|
path,
|
|
483
407
|
bridge->liftSelector,
|
|
@@ -527,28 +451,4 @@ static id RunnerTapPointerPath(
|
|
|
527
451
|
return path;
|
|
528
452
|
}
|
|
529
453
|
|
|
530
|
-
static CGPoint RunnerPointerPointAt(
|
|
531
|
-
double x,
|
|
532
|
-
double y,
|
|
533
|
-
double dx,
|
|
534
|
-
double dy,
|
|
535
|
-
double scale,
|
|
536
|
-
double degrees,
|
|
537
|
-
double baseRadius,
|
|
538
|
-
double t,
|
|
539
|
-
double side
|
|
540
|
-
) {
|
|
541
|
-
double centerX = x + dx * t;
|
|
542
|
-
double centerY = y + dy * t;
|
|
543
|
-
double startRadius = baseRadius / MAX(scale, 1.0);
|
|
544
|
-
double endRadius = baseRadius;
|
|
545
|
-
if (scale < 1.0) {
|
|
546
|
-
startRadius = baseRadius;
|
|
547
|
-
endRadius = baseRadius * scale;
|
|
548
|
-
}
|
|
549
|
-
double radius = startRadius + (endRadius - startRadius) * t;
|
|
550
|
-
double angle = (-M_PI_2) + (degrees * M_PI / 180.0) * t;
|
|
551
|
-
return CGPointMake(centerX + cos(angle) * radius * side, centerY + sin(angle) * radius * side);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
454
|
@end
|