@swmansion/argent 0.31.1-next.11 → 0.31.1-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-cmds.mjs +1 -1
- package/dist/installer.mjs +1 -1
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Commands.swift +2 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift +96 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerUITests-Bridging-Header.h +1 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentTouchSynthesizer.h +40 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentTouchSynthesizer.m +149 -0
- package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/RunnerProtocol.swift +22 -5
- package/dist/tool-server.cjs +156 -57
- package/package.json +1 -1
- package/rules/argent.md +1 -1
- package/skills/argent-create-e2e/SKILL.md +1 -1
- package/skills/argent-ios-device-interact/SKILL.md +3 -3
- package/skills/argent-qa-e2e/SKILL.md +1 -1
package/dist/cli-cmds.mjs
CHANGED
|
@@ -35740,7 +35740,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
35740
35740
|
var SESSION_ID2 = randomUUID5();
|
|
35741
35741
|
function readCliVersion() {
|
|
35742
35742
|
if (true) {
|
|
35743
|
-
return "0.31.1-next.
|
|
35743
|
+
return "0.31.1-next.12";
|
|
35744
35744
|
}
|
|
35745
35745
|
return "0.0.0";
|
|
35746
35746
|
}
|
package/dist/installer.mjs
CHANGED
|
@@ -16915,7 +16915,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
16915
16915
|
var SESSION_ID = randomUUID4();
|
|
16916
16916
|
function readCliVersion() {
|
|
16917
16917
|
if (true) {
|
|
16918
|
-
return "0.31.1-next.
|
|
16918
|
+
return "0.31.1-next.12";
|
|
16919
16919
|
}
|
|
16920
16920
|
return "0.0.0";
|
|
16921
16921
|
}
|
package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Commands.swift
CHANGED
|
@@ -159,6 +159,8 @@ extension ArgentRunnerSession {
|
|
|
159
159
|
return performLongPress(request, on: app)
|
|
160
160
|
case .drag:
|
|
161
161
|
return performDrag(request, on: app)
|
|
162
|
+
case .touchPaths:
|
|
163
|
+
return performTouchPaths(request, on: app)
|
|
162
164
|
case .type:
|
|
163
165
|
return performType(request, on: app)
|
|
164
166
|
case .keyboardReturn:
|
package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift
CHANGED
|
@@ -122,6 +122,102 @@ extension ArgentRunnerSession {
|
|
|
122
122
|
return .success(MessagePayload(message: "dragged"))
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/// Most fingers one `touchPaths` command plays. A hand has five.
|
|
126
|
+
private static let maxTouchFingers = 5
|
|
127
|
+
/// Longest `touchPaths` gesture, comfortably inside the command budget.
|
|
128
|
+
private static let maxTouchPathsMs = 30_000.0
|
|
129
|
+
|
|
130
|
+
/// Plays one path per finger, all at once: pinches, rotations, and any
|
|
131
|
+
/// single path with waypoints. XCUICoordinate only draws one straight drag,
|
|
132
|
+
/// so this goes through XCTest's event synthesizer (ArgentTouchSynthesizer).
|
|
133
|
+
///
|
|
134
|
+
/// Wire points are absolute screen points, the same space `tap` and `drag`
|
|
135
|
+
/// take, and times are measured from the start of the gesture. A finger
|
|
136
|
+
/// whose first waypoint is later than 0 touches down late, which is how a
|
|
137
|
+
/// second finger joins a gesture already under way.
|
|
138
|
+
func performTouchPaths(_ request: CommandRequest, on app: XCUIApplication)
|
|
139
|
+
-> Envelope
|
|
140
|
+
{
|
|
141
|
+
guard let fingers = request.fingers,
|
|
142
|
+
(1...Self.maxTouchFingers).contains(fingers.count)
|
|
143
|
+
else {
|
|
144
|
+
return .failure(
|
|
145
|
+
.invalidRequest,
|
|
146
|
+
"touchPaths requires between 1 and \(Self.maxTouchFingers) fingers"
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (index, finger) in fingers.enumerated() {
|
|
151
|
+
guard !finger.points.isEmpty else {
|
|
152
|
+
return .failure(
|
|
153
|
+
.invalidRequest,
|
|
154
|
+
"touchPaths finger \(index) has no waypoints"
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var previousMs = 0.0
|
|
159
|
+
|
|
160
|
+
for point in finger.points {
|
|
161
|
+
guard point.x.isFinite, point.y.isFinite, point.tMs.isFinite,
|
|
162
|
+
point.tMs >= previousMs, point.tMs <= Self.maxTouchPathsMs
|
|
163
|
+
else {
|
|
164
|
+
return .failure(
|
|
165
|
+
.invalidRequest,
|
|
166
|
+
"touchPaths finger \(index) needs finite waypoints whose times do not decrease and stay within \(Int(Self.maxTouchPathsMs)) ms"
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
previousMs = point.tMs
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if let reason = ArgentTouchSynthesizer.unavailabilityReason() {
|
|
175
|
+
return .failure(
|
|
176
|
+
.unsupportedOperation,
|
|
177
|
+
"multi-finger touch is unavailable here: \(reason)",
|
|
178
|
+
hint:
|
|
179
|
+
"Use tap, swipe or drag for a single finger, or drive the app's own zoom controls."
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The orientation the app draws in, not the way the phone is held: a
|
|
184
|
+
// portrait-only app on a phone turned sideways is still portrait. Wire
|
|
185
|
+
// points are in the app's space, which is the panel's own only in
|
|
186
|
+
// portrait. Other orientations have not been checked on hardware, and
|
|
187
|
+
// a touch that lands somewhere else without an error is worse than a
|
|
188
|
+
// refusal.
|
|
189
|
+
switch ArgentTouchSynthesizer.interfaceOrientation(ofApplication: app) {
|
|
190
|
+
case .portrait:
|
|
191
|
+
break
|
|
192
|
+
case .unknown:
|
|
193
|
+
return .failure(
|
|
194
|
+
.unsupportedOperation,
|
|
195
|
+
"touchPaths could not read the app's interface orientation",
|
|
196
|
+
hint:
|
|
197
|
+
"Use tap, swipe or drag for a single finger, or drive the app's own zoom controls."
|
|
198
|
+
)
|
|
199
|
+
default:
|
|
200
|
+
return .failure(
|
|
201
|
+
.unsupportedOperation,
|
|
202
|
+
"touchPaths has only been verified with the app drawn in portrait",
|
|
203
|
+
hint: "Turn the phone upright so the app draws in portrait, then retry."
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let waypoints = fingers.map { finger in
|
|
208
|
+
finger.points.map { [$0.x, $0.y, $0.tMs / 1000] as [NSNumber] }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if let failure = ArgentTouchSynthesizer.synthesizeFingers(
|
|
212
|
+
waypoints,
|
|
213
|
+
interfaceOrientation: .portrait
|
|
214
|
+
) {
|
|
215
|
+
return .failure(.commandFailed, failure)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return .success(MessagePayload(message: "touched"))
|
|
219
|
+
}
|
|
220
|
+
|
|
125
221
|
/// Reports the interaction viewport, the rectangle that normalized 0-1
|
|
126
222
|
/// coordinates map into.
|
|
127
223
|
func appViewport(_ app: XCUIApplication) -> Envelope {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Multi-finger touch synthesis through XCTest's own event classes.
|
|
6
|
+
///
|
|
7
|
+
/// The public XCUICoordinate API can drive one finger along one straight
|
|
8
|
+
/// press-then-drag. Pinches, rotations and any path with waypoints need several
|
|
9
|
+
/// fingers moving at once, which XCTest builds internally from
|
|
10
|
+
/// XCPointerEventPath and XCSynthesizedEventRecord. Those classes are not in
|
|
11
|
+
/// any public header, so they are looked up by name at runtime: nothing links
|
|
12
|
+
/// against a private symbol, and an Xcode that renames or drops one makes
|
|
13
|
+
/// `unavailabilityReason` answer instead of the runner failing to load.
|
|
14
|
+
@interface ArgentTouchSynthesizer : NSObject
|
|
15
|
+
|
|
16
|
+
/// nil when every class and selector this needs is present, otherwise a
|
|
17
|
+
/// sentence naming what is missing.
|
|
18
|
+
+ (NSString *_Nullable)unavailabilityReason;
|
|
19
|
+
|
|
20
|
+
/// The orientation the app's interface is drawn in, which can differ from the
|
|
21
|
+
/// way the phone is held (a portrait-only app on a phone turned sideways).
|
|
22
|
+
/// UIInterfaceOrientationUnknown when this XCTest cannot say.
|
|
23
|
+
+ (UIInterfaceOrientation)interfaceOrientationOfApplication:(id)application;
|
|
24
|
+
|
|
25
|
+
/// Plays every finger at once as one event record, and returns once the last
|
|
26
|
+
/// finger has lifted. Each finger is an array of `[x, y, seconds]` waypoints:
|
|
27
|
+
/// points in the panel's own portrait space, and the time since the gesture
|
|
28
|
+
/// started. The finger touches down at its first waypoint, moves through the
|
|
29
|
+
/// rest in order, and lifts a hair after its last. Waypoint times must not
|
|
30
|
+
/// decrease.
|
|
31
|
+
///
|
|
32
|
+
/// Returns nil on success, otherwise "Name: reason" for the failure or the
|
|
33
|
+
/// NSException XCTest threw (Swift cannot catch those).
|
|
34
|
+
+ (NSString *_Nullable)synthesizeFingers:
|
|
35
|
+
(NSArray<NSArray<NSArray<NSNumber *> *> *> *)fingers
|
|
36
|
+
interfaceOrientation:(UIInterfaceOrientation)orientation;
|
|
37
|
+
|
|
38
|
+
@end
|
|
39
|
+
|
|
40
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#import "ArgentTouchSynthesizer.h"
|
|
2
|
+
|
|
3
|
+
#import <XCTest/XCTest.h>
|
|
4
|
+
#import <objc/message.h>
|
|
5
|
+
|
|
6
|
+
/// The selectors used on XCTest's private classes, declared as protocols so the
|
|
7
|
+
/// compiler can check the calls without a class symbol to link against.
|
|
8
|
+
@protocol ArgentPointerEventPath <NSObject>
|
|
9
|
+
- (instancetype)initForTouchAtPoint:(CGPoint)point offset:(NSTimeInterval)offset;
|
|
10
|
+
- (void)moveToPoint:(CGPoint)point atOffset:(NSTimeInterval)offset;
|
|
11
|
+
- (void)liftUpAtOffset:(NSTimeInterval)offset;
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@protocol ArgentSynthesizedEventRecord <NSObject>
|
|
15
|
+
- (instancetype)initWithName:(NSString *)name
|
|
16
|
+
displayID:(NSUInteger)displayID
|
|
17
|
+
interfaceOrientation:(UIInterfaceOrientation)orientation;
|
|
18
|
+
- (instancetype)initWithName:(NSString *)name
|
|
19
|
+
interfaceOrientation:(UIInterfaceOrientation)orientation;
|
|
20
|
+
- (void)addPointerEventPath:(id<ArgentPointerEventPath>)path;
|
|
21
|
+
- (BOOL)synthesizeWithError:(NSError *_Nullable *_Nullable)error;
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
/// How long after its last waypoint a finger lifts, so the move and the lift
|
|
25
|
+
/// are two distinct events in the stream.
|
|
26
|
+
static const NSTimeInterval kLiftDelay = 0.01;
|
|
27
|
+
|
|
28
|
+
@implementation ArgentTouchSynthesizer
|
|
29
|
+
|
|
30
|
+
+ (NSString *_Nullable)unavailabilityReason {
|
|
31
|
+
Class pathClass = NSClassFromString(@"XCPointerEventPath");
|
|
32
|
+
Class recordClass = NSClassFromString(@"XCSynthesizedEventRecord");
|
|
33
|
+
|
|
34
|
+
if (pathClass == Nil || recordClass == Nil) {
|
|
35
|
+
return @"this Xcode's XCTest has no XCPointerEventPath/XCSynthesizedEventRecord";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
SEL pathSelectors[] = {
|
|
39
|
+
@selector(initForTouchAtPoint:offset:),
|
|
40
|
+
@selector(moveToPoint:atOffset:),
|
|
41
|
+
@selector(liftUpAtOffset:),
|
|
42
|
+
};
|
|
43
|
+
for (size_t i = 0; i < sizeof(pathSelectors) / sizeof(SEL); i++) {
|
|
44
|
+
if (![pathClass instancesRespondToSelector:pathSelectors[i]]) {
|
|
45
|
+
return [NSString stringWithFormat:@"XCPointerEventPath no longer responds to %@",
|
|
46
|
+
NSStringFromSelector(pathSelectors[i])];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Either record initializer will do; synthesizeFingers picks the newer one.
|
|
51
|
+
if (![recordClass instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)] &&
|
|
52
|
+
![recordClass instancesRespondToSelector:@selector(initWithName:interfaceOrientation:)]) {
|
|
53
|
+
return @"XCSynthesizedEventRecord has neither initWithName:displayID:interfaceOrientation: "
|
|
54
|
+
@"nor initWithName:interfaceOrientation:";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
SEL recordSelectors[] = {
|
|
58
|
+
@selector(addPointerEventPath:),
|
|
59
|
+
@selector(synthesizeWithError:),
|
|
60
|
+
};
|
|
61
|
+
for (size_t i = 0; i < sizeof(recordSelectors) / sizeof(SEL); i++) {
|
|
62
|
+
if (![recordClass instancesRespondToSelector:recordSelectors[i]]) {
|
|
63
|
+
return [NSString stringWithFormat:@"XCSynthesizedEventRecord no longer responds to %@",
|
|
64
|
+
NSStringFromSelector(recordSelectors[i])];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return nil;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
+ (UIInterfaceOrientation)interfaceOrientationOfApplication:(id)application {
|
|
72
|
+
SEL selector = NSSelectorFromString(@"interfaceOrientation");
|
|
73
|
+
if (![application respondsToSelector:selector]) {
|
|
74
|
+
return UIInterfaceOrientationUnknown;
|
|
75
|
+
}
|
|
76
|
+
return ((UIInterfaceOrientation(*)(id, SEL))objc_msgSend)(application, selector);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The main screen's display id, which the display-aware record initializer
|
|
80
|
+
/// routes touches by. nil when XCUIScreen does not expose one.
|
|
81
|
+
+ (NSNumber *_Nullable)mainDisplayID {
|
|
82
|
+
XCUIScreen *screen = XCUIScreen.mainScreen;
|
|
83
|
+
if (![screen respondsToSelector:NSSelectorFromString(@"displayID")]) {
|
|
84
|
+
return nil;
|
|
85
|
+
}
|
|
86
|
+
id value = [screen valueForKey:@"displayID"];
|
|
87
|
+
return [value isKindOfClass:NSNumber.class] ? value : nil;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// A new event record. Prefers the display-aware initializer, which newer
|
|
91
|
+
/// XCTest uses for its own touches, and falls back to the older one when this
|
|
92
|
+
/// XCTest or the screen cannot supply a display id.
|
|
93
|
+
+ (id<ArgentSynthesizedEventRecord>)recordWithInterfaceOrientation:
|
|
94
|
+
(UIInterfaceOrientation)orientation {
|
|
95
|
+
Class recordClass = NSClassFromString(@"XCSynthesizedEventRecord");
|
|
96
|
+
NSString *name = @"argent-touch-paths";
|
|
97
|
+
NSNumber *displayID = [self mainDisplayID];
|
|
98
|
+
|
|
99
|
+
if (displayID != nil &&
|
|
100
|
+
[recordClass instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)]) {
|
|
101
|
+
return [[recordClass alloc] initWithName:name
|
|
102
|
+
displayID:displayID.unsignedIntegerValue
|
|
103
|
+
interfaceOrientation:orientation];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [[recordClass alloc] initWithName:name interfaceOrientation:orientation];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
+ (NSString *_Nullable)synthesizeFingers:
|
|
110
|
+
(NSArray<NSArray<NSArray<NSNumber *> *> *> *)fingers
|
|
111
|
+
interfaceOrientation:(UIInterfaceOrientation)orientation {
|
|
112
|
+
NSString *unavailable = [self unavailabilityReason];
|
|
113
|
+
if (unavailable != nil) {
|
|
114
|
+
return unavailable;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@try {
|
|
118
|
+
id<ArgentSynthesizedEventRecord> record = [self recordWithInterfaceOrientation:orientation];
|
|
119
|
+
|
|
120
|
+
for (NSArray<NSArray<NSNumber *> *> *waypoints in fingers) {
|
|
121
|
+
NSArray<NSNumber *> *first = waypoints.firstObject;
|
|
122
|
+
id<ArgentPointerEventPath> path = [[NSClassFromString(@"XCPointerEventPath") alloc]
|
|
123
|
+
initForTouchAtPoint:CGPointMake(first[0].doubleValue, first[1].doubleValue)
|
|
124
|
+
offset:first[2].doubleValue];
|
|
125
|
+
|
|
126
|
+
for (NSUInteger i = 1; i < waypoints.count; i++) {
|
|
127
|
+
NSArray<NSNumber *> *waypoint = waypoints[i];
|
|
128
|
+
[path moveToPoint:CGPointMake(waypoint[0].doubleValue, waypoint[1].doubleValue)
|
|
129
|
+
atOffset:waypoint[2].doubleValue];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[path liftUpAtOffset:waypoints.lastObject[2].doubleValue + kLiftDelay];
|
|
133
|
+
[record addPointerEventPath:path];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
NSError *error = nil;
|
|
137
|
+
if (![record synthesizeWithError:&error]) {
|
|
138
|
+
return [NSString stringWithFormat:@"XCTest refused the touch events: %@",
|
|
139
|
+
error.localizedDescription ?: @"no error given"];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return nil;
|
|
143
|
+
} @catch (NSException *exception) {
|
|
144
|
+
return [NSString stringWithFormat:@"%@: %@", exception.name ?: @"NSException",
|
|
145
|
+
exception.reason ?: @"unhandled Objective-C exception"];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@end
|
|
@@ -13,6 +13,7 @@ enum CommandKind: String, Codable {
|
|
|
13
13
|
case tap
|
|
14
14
|
case longPress
|
|
15
15
|
case drag
|
|
16
|
+
case touchPaths
|
|
16
17
|
case type
|
|
17
18
|
case keyboardReturn
|
|
18
19
|
case keyboardDelete
|
|
@@ -30,8 +31,8 @@ extension CommandKind {
|
|
|
30
31
|
switch self {
|
|
31
32
|
case .status, .viewport, .snapshot, .screenshot:
|
|
32
33
|
return true
|
|
33
|
-
case .tap, .longPress, .drag, .
|
|
34
|
-
.button, .shutdown:
|
|
34
|
+
case .tap, .longPress, .drag, .touchPaths, .type, .keyboardReturn,
|
|
35
|
+
.keyboardDelete, .button, .shutdown:
|
|
35
36
|
return false
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -41,8 +42,8 @@ extension CommandKind {
|
|
|
41
42
|
/// runner's own host app.
|
|
42
43
|
var requiresAppBundleId: Bool {
|
|
43
44
|
switch self {
|
|
44
|
-
case .viewport, .tap, .longPress, .drag, .
|
|
45
|
-
.keyboardDelete, .snapshot:
|
|
45
|
+
case .viewport, .tap, .longPress, .drag, .touchPaths, .type,
|
|
46
|
+
.keyboardReturn, .keyboardDelete, .snapshot:
|
|
46
47
|
return true
|
|
47
48
|
case .status, .button, .screenshot, .shutdown:
|
|
48
49
|
return false
|
|
@@ -73,7 +74,7 @@ extension CommandKind {
|
|
|
73
74
|
// never reports quiescent stalls the wait until XCTest gives up and
|
|
74
75
|
// synthesizes the event anyway, and a shorter budget would abandon a
|
|
75
76
|
// command that still completes.
|
|
76
|
-
case .tap, .longPress, .drag: return 75
|
|
77
|
+
case .tap, .longPress, .drag, .touchPaths: return 75
|
|
77
78
|
default: return 30
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -90,6 +91,20 @@ enum DeviceButton: String, Codable {
|
|
|
90
91
|
case actionButton
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/// One point on a finger's path: an absolute point in the app's coordinate
|
|
95
|
+
/// space, and the time since the gesture started.
|
|
96
|
+
struct TouchWaypoint: Codable {
|
|
97
|
+
let x: Double
|
|
98
|
+
let y: Double
|
|
99
|
+
let tMs: Double
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// One finger of a `touchPaths` command: it touches down at the first
|
|
103
|
+
/// waypoint, moves through the rest in order, and lifts after the last.
|
|
104
|
+
struct TouchFinger: Codable {
|
|
105
|
+
let points: [TouchWaypoint]
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
struct CommandRequest: Codable {
|
|
94
109
|
let command: CommandKind
|
|
95
110
|
let commandId: String?
|
|
@@ -119,6 +134,8 @@ struct CommandRequest: Codable {
|
|
|
119
134
|
/// velocity is near zero and the scroll view does not fling. Mirrors the
|
|
120
135
|
/// simulator's ease-out `settle` swipe.
|
|
121
136
|
let settle: Bool?
|
|
137
|
+
/// `touchPaths`: one path per finger, all played at once.
|
|
138
|
+
let fingers: [TouchFinger]?
|
|
122
139
|
/// `type`: the text delivered to the focused input.
|
|
123
140
|
let text: String?
|
|
124
141
|
/// `button`: which hardware button to press.
|
package/dist/tool-server.cjs
CHANGED
|
@@ -101024,7 +101024,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
101024
101024
|
var SESSION_ID = (0, import_node_crypto3.randomUUID)();
|
|
101025
101025
|
function readCliVersion() {
|
|
101026
101026
|
if (true) {
|
|
101027
|
-
return "0.31.1-next.
|
|
101027
|
+
return "0.31.1-next.12";
|
|
101028
101028
|
}
|
|
101029
101029
|
return "0.0.0";
|
|
101030
101030
|
}
|
|
@@ -141439,6 +141439,13 @@ async function dragBetween(api, bundleId, from2, to, opts = {}) {
|
|
|
141439
141439
|
);
|
|
141440
141440
|
return { reactivated: repliedReactivated(data) };
|
|
141441
141441
|
}
|
|
141442
|
+
async function touchPaths(api, bundleId, fingers) {
|
|
141443
|
+
const data = await api.run(
|
|
141444
|
+
{ command: "touchPaths", appBundleId: bundleId, fingers },
|
|
141445
|
+
{ timeoutMs: GESTURE_TIMEOUT_MS }
|
|
141446
|
+
);
|
|
141447
|
+
return { reactivated: repliedReactivated(data) };
|
|
141448
|
+
}
|
|
141442
141449
|
async function pressButton(api, button) {
|
|
141443
141450
|
await api.run({
|
|
141444
141451
|
command: "button",
|
|
@@ -142564,13 +142571,33 @@ Pass momentum:false for a momentum-free drag that decelerates into the release,
|
|
|
142564
142571
|
|
|
142565
142572
|
// ../tool-server/src/tools/gesture-custom/index.ts
|
|
142566
142573
|
init_zod();
|
|
142567
|
-
init_device_info();
|
|
142568
|
-
init_capability();
|
|
142569
142574
|
|
|
142570
142575
|
// ../tool-server/src/utils/gesture-utils.ts
|
|
142571
142576
|
function lerp(a, b, t) {
|
|
142572
142577
|
return a + (b - a) * t;
|
|
142573
142578
|
}
|
|
142579
|
+
function pinchContacts(params, t) {
|
|
142580
|
+
const angleRad = (params.angle ?? 0) * Math.PI / 180;
|
|
142581
|
+
const cosA = Math.cos(angleRad);
|
|
142582
|
+
const sinA = Math.sin(angleRad);
|
|
142583
|
+
const halfDist = (params.startDistance + (params.endDistance - params.startDistance) * t) / 2;
|
|
142584
|
+
const cx = params.centerX + ((params.endCenterX ?? params.centerX) - params.centerX) * t;
|
|
142585
|
+
const cy = params.centerY + ((params.endCenterY ?? params.centerY) - params.centerY) * t;
|
|
142586
|
+
return {
|
|
142587
|
+
first: { x: cx - halfDist * cosA, y: cy - halfDist * sinA },
|
|
142588
|
+
second: { x: cx + halfDist * cosA, y: cy + halfDist * sinA }
|
|
142589
|
+
};
|
|
142590
|
+
}
|
|
142591
|
+
function rotateContacts(params, t) {
|
|
142592
|
+
const angleDeg = params.startAngle + (params.endAngle - params.startAngle) * t;
|
|
142593
|
+
const angleRad = angleDeg * Math.PI / 180;
|
|
142594
|
+
const dx = params.radiusX * Math.cos(angleRad);
|
|
142595
|
+
const dy = params.radiusY * Math.sin(angleRad);
|
|
142596
|
+
return {
|
|
142597
|
+
first: { x: params.centerX + dx, y: params.centerY + dy },
|
|
142598
|
+
second: { x: params.centerX - dx, y: params.centerY - dy }
|
|
142599
|
+
};
|
|
142600
|
+
}
|
|
142574
142601
|
function interpolateEvents(events, steps) {
|
|
142575
142602
|
if (events.length < 2 || steps < 1) return events;
|
|
142576
142603
|
const result = [];
|
|
@@ -142614,7 +142641,59 @@ function sendTouchEvent(api, type, x, y, x2, y2) {
|
|
|
142614
142641
|
});
|
|
142615
142642
|
}
|
|
142616
142643
|
|
|
142644
|
+
// ../tool-server/src/utils/ios-device/touch-paths.ts
|
|
142645
|
+
var FRAME_MS = 16;
|
|
142646
|
+
function frameCount(durationMs) {
|
|
142647
|
+
return Math.max(1, Math.round(durationMs / FRAME_MS));
|
|
142648
|
+
}
|
|
142649
|
+
function twoFingerPaths(viewport, durationMs, contactsAt) {
|
|
142650
|
+
const steps = frameCount(durationMs);
|
|
142651
|
+
const first = { points: [] };
|
|
142652
|
+
const second = { points: [] };
|
|
142653
|
+
for (let i = 0; i <= steps; i++) {
|
|
142654
|
+
const contacts = contactsAt(i / steps);
|
|
142655
|
+
const tMs = i * durationMs / steps;
|
|
142656
|
+
first.points.push({ ...toPoints(viewport, contacts.first.x, contacts.first.y), tMs });
|
|
142657
|
+
second.points.push({ ...toPoints(viewport, contacts.second.x, contacts.second.y), tMs });
|
|
142658
|
+
}
|
|
142659
|
+
return [first, second];
|
|
142660
|
+
}
|
|
142661
|
+
function pinchPaths(params, viewport, durationMs) {
|
|
142662
|
+
return twoFingerPaths(viewport, durationMs, (t) => pinchContacts(params, t));
|
|
142663
|
+
}
|
|
142664
|
+
function rotatePaths(params, viewport, durationMs) {
|
|
142665
|
+
return twoFingerPaths(viewport, durationMs, (t) => rotateContacts(params, t));
|
|
142666
|
+
}
|
|
142667
|
+
function trainPaths(events, viewport) {
|
|
142668
|
+
const down = events[0];
|
|
142669
|
+
const up = events[events.length - 1];
|
|
142670
|
+
if (down?.type !== "Down" || up?.type !== "Up" || events.length < 2) {
|
|
142671
|
+
throw new Error("A touch train starts with a Down and ends with an Up.");
|
|
142672
|
+
}
|
|
142673
|
+
if (events.slice(1, -1).some((event2) => event2.type !== "Move")) {
|
|
142674
|
+
throw new Error("A touch train has only Move events between its Down and its Up.");
|
|
142675
|
+
}
|
|
142676
|
+
const first = { points: [] };
|
|
142677
|
+
const second = { points: [] };
|
|
142678
|
+
let tMs = 0;
|
|
142679
|
+
events.forEach((event2, index) => {
|
|
142680
|
+
if (event2.x2 === void 0 !== (event2.y2 === void 0)) {
|
|
142681
|
+
throw new Error(`Event ${index} has only one of x2 and y2; give both or neither.`);
|
|
142682
|
+
}
|
|
142683
|
+
if (index > 0) {
|
|
142684
|
+
tMs += event2.delayMs ?? FRAME_MS;
|
|
142685
|
+
}
|
|
142686
|
+
first.points.push({ ...toPoints(viewport, event2.x, event2.y), tMs });
|
|
142687
|
+
if (event2.x2 !== void 0 && event2.y2 !== void 0) {
|
|
142688
|
+
second.points.push({ ...toPoints(viewport, event2.x2, event2.y2), tMs });
|
|
142689
|
+
}
|
|
142690
|
+
});
|
|
142691
|
+
return second.points.length > 0 ? [first, second] : [first];
|
|
142692
|
+
}
|
|
142693
|
+
|
|
142617
142694
|
// ../tool-server/src/tools/gesture-custom/index.ts
|
|
142695
|
+
init_device_info();
|
|
142696
|
+
init_capability();
|
|
142618
142697
|
var sleep6 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
142619
142698
|
var eventSchema = external_exports.object({
|
|
142620
142699
|
type: external_exports.enum(["Down", "Move", "Up"]).describe("Touch event type"),
|
|
@@ -142643,32 +142722,42 @@ function samePoint(a, b) {
|
|
|
142643
142722
|
return Math.abs(a.x - b.x) <= SAME_POINT_EPSILON && Math.abs(a.y - b.y) <= SAME_POINT_EPSILON;
|
|
142644
142723
|
}
|
|
142645
142724
|
function planIosDeviceGesture(events) {
|
|
142646
|
-
if (events.some((e) => e.x2 !== void 0 || e.y2 !== void 0)) {
|
|
142647
|
-
return {
|
|
142648
|
-
kind: "unsupported",
|
|
142649
|
-
reason: "gesture-custom on a physical iOS device supports single-finger gestures only; two-finger event trains (pinch/rotate) have no XCTest coordinate API."
|
|
142650
|
-
};
|
|
142651
|
-
}
|
|
142652
142725
|
const down = events[0];
|
|
142653
142726
|
const up = events[events.length - 1];
|
|
142654
|
-
|
|
142655
|
-
const pickup = down !== void 0 && hold !== void 0 && hold.type === "Move" && samePoint(down, hold);
|
|
142656
|
-
if (down === void 0 || up === void 0 || down.type !== "Down" || up.type !== "Up" || !(events.length === 2 || events.length === 3 && pickup)) {
|
|
142727
|
+
if (down === void 0 || up === void 0 || events.length < 2 || down.type !== "Down" || up.type !== "Up" || events.slice(1, -1).some((event2) => event2.type !== "Move")) {
|
|
142657
142728
|
return {
|
|
142658
142729
|
kind: "unsupported",
|
|
142659
|
-
reason: "gesture-custom on a physical iOS device
|
|
142730
|
+
reason: "gesture-custom on a physical iOS device takes a Down, any number of Moves, then an Up (a second finger rides along as x2/y2 on those events)."
|
|
142660
142731
|
};
|
|
142661
142732
|
}
|
|
142733
|
+
const twoFingers = events.some((e) => e.x2 !== void 0 || e.y2 !== void 0);
|
|
142734
|
+
const hold = events.length === 3 ? events[1] : void 0;
|
|
142735
|
+
const pickup = hold !== void 0 && hold.type === "Move" && samePoint(down, hold);
|
|
142736
|
+
if (twoFingers || !(events.length === 2 || events.length === 3 && pickup)) {
|
|
142737
|
+
return { kind: "paths" };
|
|
142738
|
+
}
|
|
142662
142739
|
return { kind: "gesture", down, up, ...pickup && hold ? { hold } : {} };
|
|
142663
142740
|
}
|
|
142664
|
-
async function runOnIosDevice(runner, udid, events) {
|
|
142741
|
+
async function runOnIosDevice(runner, udid, events, interpolate) {
|
|
142665
142742
|
const plan = planIosDeviceGesture(events);
|
|
142666
142743
|
if (plan.kind === "unsupported") {
|
|
142667
142744
|
throw new InvalidToolInputError(plan.reason);
|
|
142668
142745
|
}
|
|
142669
|
-
const { down, hold, up } = plan;
|
|
142670
142746
|
const bundleId = requireCurrentIosDeviceApp(udid);
|
|
142671
142747
|
const viewport = await getViewport(runner, bundleId);
|
|
142748
|
+
if (plan.kind === "paths") {
|
|
142749
|
+
const played = interpolate && interpolate > 0 ? interpolateEvents(events, interpolate) : events;
|
|
142750
|
+
let fingers;
|
|
142751
|
+
try {
|
|
142752
|
+
fingers = trainPaths(played, viewport);
|
|
142753
|
+
} catch (error63) {
|
|
142754
|
+
throw new InvalidToolInputError(error63.message);
|
|
142755
|
+
}
|
|
142756
|
+
const reply = await touchPaths(runner, bundleId, fingers);
|
|
142757
|
+
const reactivated2 = viewport.reactivated === true || reply.reactivated;
|
|
142758
|
+
return { events: events.length, ...reactivated2 ? { reactivated: true } : {} };
|
|
142759
|
+
}
|
|
142760
|
+
const { down, hold, up } = plan;
|
|
142672
142761
|
const holdMs = hold ? hold.delayMs ?? 16 : up.delayMs ?? 16;
|
|
142673
142762
|
const moveMs = hold ? up.delayMs ?? 16 : void 0;
|
|
142674
142763
|
const gesture = samePoint(down, up) ? await longPressAt(
|
|
@@ -142699,7 +142788,7 @@ For simple taps use the gesture-tap tool. For straight-line scrolling use the ge
|
|
|
142699
142788
|
For pinch gestures use gesture-pinch. For rotation gestures use gesture-rotate.
|
|
142700
142789
|
All x/y values are normalized 0.0\u20131.0 (screen fractions, not pixels). delayMs controls the delay before each event (default 16ms \u2248 60fps).
|
|
142701
142790
|
Set interpolate to auto-generate smooth intermediate Move events between your keyframes.
|
|
142702
|
-
Physical iOS: Down then Up
|
|
142791
|
+
Physical iOS: a Down, any Moves, then an Up, with an optional second finger as x2/y2. Paths need the app drawn in portrait. A plain Down then Up keeps its hold semantics: same point = press-hold, other point = drag (Up delayMs = rest before the move), and one Move at the Down point between them = long-press pickup (Move delayMs = hold, Up delayMs = move time). Anything else (more Moves, a second finger) plays as timed paths, each event waiting its delayMs after the one before. The second finger touches down at the first event with x2/y2 and lifts after the last.
|
|
142703
142792
|
Returns { events: number } with the total count of events dispatched. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the target device is not booted or an event type is invalid.
|
|
142704
142793
|
|
|
142705
142794
|
Example long-press at center:
|
|
@@ -142719,11 +142808,11 @@ Example pinch-to-zoom (with interpolate:10 for smoothness):
|
|
|
142719
142808
|
interpolate: 10`,
|
|
142720
142809
|
zodSchema: zodSchema21,
|
|
142721
142810
|
capability: capability13,
|
|
142722
|
-
// Declare the runner only for a train execute can
|
|
142811
|
+
// Declare the runner only for a train execute can play. A rejected train must not pay a runner cold start.
|
|
142723
142812
|
services: (params) => {
|
|
142724
142813
|
const device = resolveDevice(params.udid);
|
|
142725
142814
|
if (isIosPhysicalDevice(device)) {
|
|
142726
|
-
return planIosDeviceGesture(params.events).kind
|
|
142815
|
+
return planIosDeviceGesture(params.events).kind !== "unsupported" ? { iosDeviceRunner: iosDeviceRunnerRef(device) } : {};
|
|
142727
142816
|
}
|
|
142728
142817
|
return { simulatorServer: simulatorServerRef(device) };
|
|
142729
142818
|
},
|
|
@@ -142733,7 +142822,8 @@ Example pinch-to-zoom (with interpolate:10 for smoothness):
|
|
|
142733
142822
|
return runOnIosDevice(
|
|
142734
142823
|
services.iosDeviceRunner,
|
|
142735
142824
|
device.id,
|
|
142736
|
-
params.events
|
|
142825
|
+
params.events,
|
|
142826
|
+
params.interpolate
|
|
142737
142827
|
);
|
|
142738
142828
|
}
|
|
142739
142829
|
const api = services.simulatorServer;
|
|
@@ -142786,23 +142876,11 @@ var zodSchema22 = external_exports.object({
|
|
|
142786
142876
|
)
|
|
142787
142877
|
});
|
|
142788
142878
|
var capability14 = {
|
|
142789
|
-
apple: { simulator: true },
|
|
142879
|
+
apple: { simulator: true, device: true },
|
|
142790
142880
|
appleRemote: { simulator: true },
|
|
142791
142881
|
android: { emulator: true, device: true, unknown: true },
|
|
142792
142882
|
harmony: { device: true }
|
|
142793
142883
|
};
|
|
142794
|
-
function contactsAt(params, t) {
|
|
142795
|
-
const angleRad = (params.angle ?? 0) * Math.PI / 180;
|
|
142796
|
-
const cosA = Math.cos(angleRad);
|
|
142797
|
-
const sinA = Math.sin(angleRad);
|
|
142798
|
-
const halfDist = (params.startDistance + (params.endDistance - params.startDistance) * t) / 2;
|
|
142799
|
-
const cx = params.centerX + ((params.endCenterX ?? params.centerX) - params.centerX) * t;
|
|
142800
|
-
const cy = params.centerY + ((params.endCenterY ?? params.centerY) - params.centerY) * t;
|
|
142801
|
-
return {
|
|
142802
|
-
first: { x: cx - halfDist * cosA, y: cy - halfDist * sinA },
|
|
142803
|
-
second: { x: cx + halfDist * cosA, y: cy + halfDist * sinA }
|
|
142804
|
-
};
|
|
142805
|
-
}
|
|
142806
142884
|
var gesturePinchTool = {
|
|
142807
142885
|
id: "gesture-pinch",
|
|
142808
142886
|
interaction: {
|
|
@@ -142814,12 +142892,13 @@ var gesturePinchTool = {
|
|
|
142814
142892
|
startDistance > endDistance = pinch in (zoom out). startDistance < endDistance = pinch out (zoom in).
|
|
142815
142893
|
Typical values: startDistance 0.2, endDistance 0.6 for a zoom-in pinch at screen center.
|
|
142816
142894
|
Auto-generates interpolated frames at ~60fps<flag:harmonyos> on iOS and Android; HarmonyOS takes both fingers' endpoints in one call and interpolates them on-device</flag:harmonyos>. The angle parameter controls the axis (0 = horizontal, 90 = vertical). Optional endCenterX/endCenterY drift the centroid linearly over the gesture (omitted = fixed center).
|
|
142817
|
-
Use when you need to zoom in or out on a map, image, or zoomable view. Returns { pinched: true, timestampMs }. Fails if the simulator-server / emulator backend<flag:harmonyos>, or \`hdc\` on HarmonyOS,</flag:harmonyos> is not reachable for the given device.`,
|
|
142895
|
+
Use when you need to zoom in or out on a map, image, or zoomable view. Physical iPhones play both fingers through the runner as one event stream. Returns { pinched: true, timestampMs }. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the simulator-server / emulator backend<flag:harmonyos>, or \`hdc\` on HarmonyOS,</flag:harmonyos> is not reachable for the given device, or on a physical iPhone when the app is not drawn in portrait.`,
|
|
142818
142896
|
zodSchema: zodSchema22,
|
|
142819
142897
|
capability: capability14,
|
|
142820
142898
|
services: (params) => {
|
|
142821
142899
|
const device = resolveDevice(params.udid);
|
|
142822
142900
|
if (device.platform === "harmony") return {};
|
|
142901
|
+
if (isIosPhysicalDevice(device)) return { iosDeviceRunner: iosDeviceRunnerRef(device) };
|
|
142823
142902
|
return { simulatorServer: simulatorServerRef(device) };
|
|
142824
142903
|
},
|
|
142825
142904
|
async execute(services, params, ctx) {
|
|
@@ -142844,8 +142923,8 @@ Use when you need to zoom in or out on a map, image, or zoomable view. Returns {
|
|
|
142844
142923
|
)
|
|
142845
142924
|
);
|
|
142846
142925
|
assertHarmonyDisplayReady(live, "pinch");
|
|
142847
|
-
const start2 =
|
|
142848
|
-
const end =
|
|
142926
|
+
const start2 = pinchContacts(params, 0);
|
|
142927
|
+
const end = pinchContacts(params, 1);
|
|
142849
142928
|
timestampMs2 = Date.now();
|
|
142850
142929
|
await ui.multiTouchMove(
|
|
142851
142930
|
[
|
|
@@ -142865,13 +142944,26 @@ Use when you need to zoom in or out on a map, image, or zoomable view. Returns {
|
|
|
142865
142944
|
);
|
|
142866
142945
|
return { pinched: true, timestampMs: timestampMs2 };
|
|
142867
142946
|
}
|
|
142947
|
+
if (isIosPhysicalDevice(device)) {
|
|
142948
|
+
const runner = services.iosDeviceRunner;
|
|
142949
|
+
const bundleId = requireCurrentIosDeviceApp(device.id);
|
|
142950
|
+
const viewport = await getViewport(runner, bundleId);
|
|
142951
|
+
const timestampMs2 = Date.now();
|
|
142952
|
+
const { reactivated } = await touchPaths(
|
|
142953
|
+
runner,
|
|
142954
|
+
bundleId,
|
|
142955
|
+
pinchPaths(params, viewport, duration3)
|
|
142956
|
+
);
|
|
142957
|
+
const fronted = viewport.reactivated === true || reactivated;
|
|
142958
|
+
return { pinched: true, timestampMs: timestampMs2, ...fronted ? { reactivated: true } : {} };
|
|
142959
|
+
}
|
|
142868
142960
|
const api = services.simulatorServer;
|
|
142869
142961
|
const steps = Math.max(1, Math.round(duration3 / 16));
|
|
142870
142962
|
let timestampMs = 0;
|
|
142871
142963
|
let warning;
|
|
142872
142964
|
for (let i = 0; i <= steps; i++) {
|
|
142873
142965
|
const t = i / steps;
|
|
142874
|
-
const { first, second } =
|
|
142966
|
+
const { first, second } = pinchContacts(params, t);
|
|
142875
142967
|
const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
|
|
142876
142968
|
if (i === 0) timestampMs = Date.now();
|
|
142877
142969
|
const sent = await sendTouchEvent(api, type, first.x, first.y, second.x, second.y);
|
|
@@ -142911,7 +143003,7 @@ var zodSchema23 = external_exports.object({
|
|
|
142911
143003
|
message: "Pass radius, or both radiusX and radiusY."
|
|
142912
143004
|
});
|
|
142913
143005
|
var capability15 = {
|
|
142914
|
-
apple: { simulator: true },
|
|
143006
|
+
apple: { simulator: true, device: true },
|
|
142915
143007
|
appleRemote: { simulator: true },
|
|
142916
143008
|
android: { emulator: true, device: true, unknown: true }
|
|
142917
143009
|
};
|
|
@@ -142934,19 +143026,35 @@ var gestureRotateTool = {
|
|
|
142934
143026
|
endAngle > startAngle = clockwise rotation. Typical values: radius 0.15, startAngle 0, endAngle 90 for a 90\xB0 clockwise turn. A single radius applies to both axes, so on a non-square screen it traces a physical ellipse (finger separation varies through the turn); pass radiusX+radiusY (fractions of width/height with radiusX\xB7width = radiusY\xB7height) for a physically circular orbit instead.
|
|
142935
143027
|
Auto-generates interpolated frames at ~60fps.
|
|
142936
143028
|
Unlike gesture-pinch which moves fingers linearly to zoom, this orbits fingers in an arc to change orientation.
|
|
142937
|
-
Use when you need to rotate a map, image picker, or any rotateable UI element. Returns { rotated: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.
|
|
143029
|
+
Use when you need to rotate a map, image picker, or any rotateable UI element. Returns { rotated: true, timestampMs }. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the simulator-server / emulator backend is not reachable for the given device, or on a physical iPhone when the app is not drawn in portrait.
|
|
142938
143030
|
Size the orbit with radius, or with radiusX and radiusY together (the pair overrides radius); one half of the pair alone, or none of the three, is rejected.`,
|
|
142939
143031
|
zodSchema: zodSchema23,
|
|
142940
143032
|
capability: capability15,
|
|
142941
|
-
services: (params) =>
|
|
142942
|
-
|
|
142943
|
-
|
|
143033
|
+
services: (params) => {
|
|
143034
|
+
const device = resolveDevice(params.udid);
|
|
143035
|
+
return isIosPhysicalDevice(device) ? { iosDeviceRunner: iosDeviceRunnerRef(device) } : { simulatorServer: simulatorServerRef(device) };
|
|
143036
|
+
},
|
|
142944
143037
|
async execute(services, params, ctx) {
|
|
142945
|
-
const api = services.simulatorServer;
|
|
142946
143038
|
const duration3 = params.durationMs ?? 300;
|
|
142947
143039
|
const steps = Math.max(1, Math.round(duration3 / 16));
|
|
142948
143040
|
const radiusX = params.radiusX ?? params.radius;
|
|
142949
143041
|
const radiusY = params.radiusY ?? params.radius;
|
|
143042
|
+
const geometry = { ...params, radiusX, radiusY };
|
|
143043
|
+
const device = resolveDevice(params.udid);
|
|
143044
|
+
if (isIosPhysicalDevice(device)) {
|
|
143045
|
+
const runner = services.iosDeviceRunner;
|
|
143046
|
+
const bundleId = requireCurrentIosDeviceApp(device.id);
|
|
143047
|
+
const viewport = await getViewport(runner, bundleId);
|
|
143048
|
+
const timestampMs2 = Date.now();
|
|
143049
|
+
const { reactivated } = await touchPaths(
|
|
143050
|
+
runner,
|
|
143051
|
+
bundleId,
|
|
143052
|
+
rotatePaths(geometry, viewport, duration3)
|
|
143053
|
+
);
|
|
143054
|
+
const fronted = viewport.reactivated === true || reactivated;
|
|
143055
|
+
return { rotated: true, timestampMs: timestampMs2, ...fronted ? { reactivated: true } : {} };
|
|
143056
|
+
}
|
|
143057
|
+
const api = services.simulatorServer;
|
|
142950
143058
|
let timestampMs = 0;
|
|
142951
143059
|
let warning;
|
|
142952
143060
|
let lastX1 = 0;
|
|
@@ -142968,13 +143076,10 @@ Size the orbit with radius, or with radiusX and radiusY together (the pair overr
|
|
|
142968
143076
|
}
|
|
142969
143077
|
throw err;
|
|
142970
143078
|
}
|
|
142971
|
-
const
|
|
142972
|
-
|
|
142973
|
-
|
|
142974
|
-
|
|
142975
|
-
const y1 = params.centerY + radiusY * Math.sin(angleRad);
|
|
142976
|
-
const x2 = params.centerX - radiusX * Math.cos(angleRad);
|
|
142977
|
-
const y2 = params.centerY - radiusY * Math.sin(angleRad);
|
|
143079
|
+
const {
|
|
143080
|
+
first: { x: x1, y: y1 },
|
|
143081
|
+
second: { x: x2, y: y2 }
|
|
143082
|
+
} = rotateContacts(geometry, i / steps);
|
|
142978
143083
|
const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
|
|
142979
143084
|
if (i === 0) timestampMs = Date.now();
|
|
142980
143085
|
const sent = await sendTouchEvent(api, type, x1, y1, x2, y2);
|
|
@@ -151291,7 +151396,7 @@ function createRunSequenceTool(registry2) {
|
|
|
151291
151396
|
failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to run interaction sequence: ${failureSignal2.error_code}`
|
|
151292
151397
|
},
|
|
151293
151398
|
description: `Execute multiple device interaction steps in a single call (iOS simulator or physical device, Android emulator, Apple TV / Android TV, <flag:harmonyos>HarmonyOS device, </flag:harmonyos>or Chromium app).
|
|
151294
|
-
On a physical iOS device only gesture-tap, gesture-swipe, gesture-custom, button, keyboard, and await-ui-element steps run; others fail at their own gate.
|
|
151399
|
+
On a physical iOS device only gesture-tap, gesture-swipe, gesture-custom, gesture-pinch, gesture-rotate, button, keyboard, and await-ui-element steps run; others fail at their own gate.
|
|
151295
151400
|
Use when you need sequential actions and do NOT need to observe the screen between them
|
|
151296
151401
|
(e.g. scrolling multiple times, typing then pressing enter, rotating back and forth).
|
|
151297
151402
|
Returns { completed, total, steps } with per-step results. Fails if an unrecognised tool name is used in a step (error returned at that step, execution stops).
|
|
@@ -153865,7 +153970,7 @@ function formatDescribeTree(root, opts) {
|
|
|
153865
153970
|
);
|
|
153866
153971
|
} else {
|
|
153867
153972
|
header.push(
|
|
153868
|
-
|
|
153973
|
+
"Pass them straight to gesture-tap / gesture-swipe / gesture-pinch, which expect this same space."
|
|
153869
153974
|
);
|
|
153870
153975
|
header.push(
|
|
153871
153976
|
"To tap an element, use its centre: tap_x = frame.x + frame.width / 2, tap_y = frame.y + frame.height / 2."
|
|
@@ -169569,12 +169674,6 @@ async function runDirective(env, step) {
|
|
|
169569
169674
|
reason: step.kind === "long-press" ? "long-press is unsupported on HarmonyOS \u2014 `uitest uiInput` has a `longClick` verb, but it takes no hold duration and argent drives holds through `gesture-custom`, which has no HarmonyOS backend; use `tap`" : "rotate is unsupported on HarmonyOS \u2014 `uinput -T -m` is the platform's only multi-contact injection and it moves each finger along ONE straight line per call, with no way to chain segments into an arc; drive the app's own rotate control with `tap`, or `pinch`, which is a straight-line gesture and does run here"
|
|
169570
169675
|
};
|
|
169571
169676
|
}
|
|
169572
|
-
if ((step.kind === "pinch" || step.kind === "rotate") && isIosPhysicalDevice(env.device)) {
|
|
169573
|
-
return {
|
|
169574
|
-
ok: false,
|
|
169575
|
-
reason: step.kind === "pinch" ? "pinch is unsupported on a physical iOS device: XCTest exposes no two-finger coordinate API on hardware; run this e2e test on a simulator or drive the app's zoom UI with tap steps instead" : "rotate is unsupported on a physical iOS device: XCTest exposes no two-finger coordinate API on hardware; run this e2e test on a simulator or drive the app's rotate controls with tap steps instead"
|
|
169576
|
-
};
|
|
169577
|
-
}
|
|
169578
169677
|
if (step.kind === "idle" && !supportsFlowTree(env.device.platform)) {
|
|
169579
169678
|
return {
|
|
169580
169679
|
ok: false,
|
package/package.json
CHANGED
package/rules/argent.md
CHANGED
|
@@ -116,7 +116,7 @@ When: Beginning a task that involves the Android emulator, no emulator running y
|
|
|
116
116
|
|
|
117
117
|
PHYSICAL iPHONE (USB)
|
|
118
118
|
Skills: `argent-ios-device-setup` (cable, trust, signing), then `argent-ios-device-interact` (the app-scoped interaction contract)
|
|
119
|
-
When: The user names a physical iPhone, a real device, or hardware, or the target `list-devices` iOS entry has kind `"device"`. Never for a simulator, and never because a cabled phone is listed first. On hardware every interaction starts with `launch-app`; `paste`, `settings-permissions`,
|
|
119
|
+
When: The user names a physical iPhone, a real device, or hardware, or the target `list-devices` iOS entry has kind `"device"`. Never for a simulator, and never because a cabled phone is listed first. On hardware every interaction starts with `launch-app`; `paste`, `settings-permissions`, `rotate`, `shake`, screen recording, `debugger-*`, `react-profiler-*`, `native-profiler-*` and `native-*` do not exist there.
|
|
120
120
|
Prompt keywords: physical iPhone, real device, on my phone, USB, hardware
|
|
121
121
|
|
|
122
122
|
TAPPING, SWIPING, TYPING, GESTURES, SCREENSHOTS, SCROLLING
|
|
@@ -13,7 +13,7 @@ For a saved QA test case, ticket, or acceptance criterion, load `argent-qa-e2e`
|
|
|
13
13
|
|
|
14
14
|
- Before creating or changing an e2e test, read [Live authoring](references/live-authoring.md) completely.
|
|
15
15
|
- When polishing, composing, or manually reviewing YAML, read [E2E YAML](references/e2e-yaml.md). For Vega, read its platform limits before recording remote or keyboard tools.
|
|
16
|
-
- E2E tests run on physical iPhones (an iOS `list-devices` entry with kind `"device"`), but replay never auto-binds one, even when no simulator is booted: pass the phone's udid as `device` (CLI `--device`), and only a `connected` phone can run. `pinch`/`rotate` steps
|
|
16
|
+
- E2E tests run on physical iPhones (an iOS `list-devices` entry with kind `"device"`), but replay never auto-binds one, even when no simulator is booted: pass the phone's udid as `device` (CLI `--device`), and only a `connected` phone can run. `pinch`/`rotate` steps run there as two-finger gestures, in apps drawn in portrait. On hardware the runner tree is the `describe` tree: same ids and roles, no UIView hierarchy. See `argent-ios-device-interact` for the hardware contract.
|
|
17
17
|
- On capture warnings, raw coordinates, unavailable trees, mistimed transitions, overlays, or replay failures, read [Reliability and recovery](references/reliability-and-recovery.md).
|
|
18
18
|
|
|
19
19
|
## Non-negotiable rules
|
|
@@ -11,10 +11,10 @@ Read this only for a physical iPhone. `argent-device-interact` still applies for
|
|
|
11
11
|
|
|
12
12
|
- Observation never changes the screen; mutation may. `describe` and `await-ui-element` fail on a backgrounded target instead of re-fronting it; `await-screen-idle` returns `settled: false` with no reason. The first `describe` within a second of `button home` can still return the old tree; describe again. Gestures and `keyboard` re-front it and return `reactivated: true`; re-describe before the next step.
|
|
13
13
|
- `launch-app`, `open-url` and `restart-app` set the app under automation; `reinstall-app` clears it; a tool-server restart forgets it: launch again.
|
|
14
|
-
- Each tool's description states its own hardware limits (named keys,
|
|
14
|
+
- Each tool's description states its own hardware limits (named keys, buttons, edge swipes, tap counts). A gated tool fails with a bare `not supported on ios device`: the fix is in the list below, not in the error.
|
|
15
15
|
|
|
16
16
|
## Tools on hardware
|
|
17
17
|
|
|
18
|
-
- Only these exist: `list-devices`, `launch-app`, `restart-app`, `reinstall-app`, `open-url`, `describe`, `screenshot`, `screenshot-diff`, `gesture-tap`, `gesture-swipe`, `gesture-custom`, `button` (`home`, `volumeUp`, `volumeDown`, `actionButton` on models that have one), `keyboard`, `await-ui-element`, `await-screen-idle`, `run-sequence`, the e2e tools, `stop-simulator-server` and `stop-all-simulator-servers` (session end). Every other device tool fails with `not supported on ios device`.
|
|
19
|
-
- No
|
|
18
|
+
- Only these exist: `list-devices`, `launch-app`, `restart-app`, `reinstall-app`, `open-url`, `describe`, `screenshot`, `screenshot-diff`, `gesture-tap`, `gesture-swipe`, `gesture-custom`, `gesture-pinch`, `gesture-rotate`, `button` (`home`, `volumeUp`, `volumeDown`, `actionButton` on models that have one), `keyboard`, `await-ui-element`, `await-screen-idle`, `run-sequence`, the e2e tools, `stop-simulator-server` and `stop-all-simulator-servers` (session end). Every other device tool fails with `not supported on ios device`.
|
|
19
|
+
- No `rotate` (the orientation tool), `shake`, `paste`, `settings-permissions`, screen recording, `debugger-*`, `react-profiler-*`, `native-profiler-*`, `native-*`, `boot-device`: move the phone by hand, type with `keyboard`, change permissions in the phone's Settings, and debug, profile or record on a simulator.
|
|
20
20
|
- `gesture-swipe`: `durationMs` sets drag speed, not time; `momentum:false` only rests 300 ms at the end, no damping. `open-url` https lands in Safari, never in the app that owns the link: pass `bundleId`.
|
|
@@ -11,7 +11,7 @@ Load `argent-create-e2e` as the authoring engine. Follow its required references
|
|
|
11
11
|
|
|
12
12
|
**Apple TV and Android TV are out of scope.** The runner does not reject touch directives there, so they fail at the gesture layer instead of with authoring guidance. Use `argent-tv-interact` and report the limitation.
|
|
13
13
|
|
|
14
|
-
**Physical iPhones** run QA e2e tests, with
|
|
14
|
+
**Physical iPhones** run QA e2e tests, with two hardware limits: replay never auto-binds a phone, so pass its udid as `device` (CLI `--device`) and keep it `connected`; the runner tree is the `describe` tree (same ids and roles), so a selector authored on a simulator can miss there. `pinch`/`rotate` steps run as two-finger gestures, in apps drawn in portrait. Read `argent-ios-device-interact` for the app-scoped contract before recording.
|
|
15
15
|
|
|
16
16
|
## Definition of done
|
|
17
17
|
|