@panicgit/android-test-pilot 0.1.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.
Files changed (65) hide show
  1. package/.claude/skills/atp/analyze-app/SKILL.md +86 -0
  2. package/.claude/skills/atp/app-map/SKILL.md +36 -0
  3. package/.claude/skills/atp/check-logs/SKILL.md +77 -0
  4. package/.claude/skills/atp/run-test/SKILL.md +92 -0
  5. package/README.ko.md +241 -0
  6. package/README.md +241 -0
  7. package/lib/android.d.ts +96 -0
  8. package/lib/android.js +740 -0
  9. package/lib/android.js.map +1 -0
  10. package/lib/image-utils.d.ts +28 -0
  11. package/lib/image-utils.js +156 -0
  12. package/lib/image-utils.js.map +1 -0
  13. package/lib/index.d.ts +2 -0
  14. package/lib/index.js +90 -0
  15. package/lib/index.js.map +1 -0
  16. package/lib/ios.d.ts +54 -0
  17. package/lib/ios.js +241 -0
  18. package/lib/ios.js.map +1 -0
  19. package/lib/iphone-simulator.d.ts +34 -0
  20. package/lib/iphone-simulator.js +227 -0
  21. package/lib/iphone-simulator.js.map +1 -0
  22. package/lib/logger.d.ts +2 -0
  23. package/lib/logger.js +23 -0
  24. package/lib/logger.js.map +1 -0
  25. package/lib/mobile-device.d.ts +25 -0
  26. package/lib/mobile-device.js +141 -0
  27. package/lib/mobile-device.js.map +1 -0
  28. package/lib/mobilecli.d.ts +32 -0
  29. package/lib/mobilecli.js +113 -0
  30. package/lib/mobilecli.js.map +1 -0
  31. package/lib/png.d.ts +9 -0
  32. package/lib/png.js +20 -0
  33. package/lib/png.js.map +1 -0
  34. package/lib/robot.d.ts +116 -0
  35. package/lib/robot.js +10 -0
  36. package/lib/robot.js.map +1 -0
  37. package/lib/server.d.ts +3 -0
  38. package/lib/server.js +692 -0
  39. package/lib/server.js.map +1 -0
  40. package/lib/tiers/abstract-tier.d.ts +48 -0
  41. package/lib/tiers/abstract-tier.js +35 -0
  42. package/lib/tiers/abstract-tier.js.map +1 -0
  43. package/lib/tiers/screenshot-tier.d.ts +19 -0
  44. package/lib/tiers/screenshot-tier.js +53 -0
  45. package/lib/tiers/screenshot-tier.js.map +1 -0
  46. package/lib/tiers/text-tier.d.ts +20 -0
  47. package/lib/tiers/text-tier.js +138 -0
  48. package/lib/tiers/text-tier.js.map +1 -0
  49. package/lib/tiers/tier-runner.d.ts +27 -0
  50. package/lib/tiers/tier-runner.js +91 -0
  51. package/lib/tiers/tier-runner.js.map +1 -0
  52. package/lib/tiers/types.d.ts +100 -0
  53. package/lib/tiers/types.js +12 -0
  54. package/lib/tiers/types.js.map +1 -0
  55. package/lib/tiers/uiautomator-tier.d.ts +16 -0
  56. package/lib/tiers/uiautomator-tier.js +91 -0
  57. package/lib/tiers/uiautomator-tier.js.map +1 -0
  58. package/lib/utils.d.ts +4 -0
  59. package/lib/utils.js +81 -0
  60. package/lib/utils.js.map +1 -0
  61. package/lib/webdriver-agent.d.ts +45 -0
  62. package/lib/webdriver-agent.js +400 -0
  63. package/lib/webdriver-agent.js.map +1 -0
  64. package/package.json +50 -0
  65. package/templates/scenario.md +49 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * UiAutomatorTier (Tier 2) — UI hierarchy-based detection
3
+ *
4
+ * Dumps the current View hierarchy via uiautomator and searches
5
+ * for elements by resource-id or text. Can also perform tap actions.
6
+ *
7
+ * Used when Tier 1 (text-based) cannot determine the result.
8
+ */
9
+ import { AbstractTier } from "./abstract-tier";
10
+ import { TierContext, TierResult } from "./types";
11
+ export declare class UiAutomatorTier extends AbstractTier {
12
+ readonly name = "uiautomator";
13
+ readonly priority = 2;
14
+ canHandle(context: TierContext): Promise<boolean>;
15
+ execute(context: TierContext): Promise<TierResult>;
16
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * UiAutomatorTier (Tier 2) — UI hierarchy-based detection
4
+ *
5
+ * Dumps the current View hierarchy via uiautomator and searches
6
+ * for elements by resource-id or text. Can also perform tap actions.
7
+ *
8
+ * Used when Tier 1 (text-based) cannot determine the result.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.UiAutomatorTier = void 0;
12
+ const abstract_tier_1 = require("./abstract-tier");
13
+ class UiAutomatorTier extends abstract_tier_1.AbstractTier {
14
+ name = "uiautomator";
15
+ priority = 2;
16
+ async canHandle(context) {
17
+ try {
18
+ const robot = this.getAndroidRobot(context);
19
+ // Verify uiautomator is responsive
20
+ void robot.adb("shell", "echo", "ping");
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async execute(context) {
28
+ const robot = this.getAndroidRobot(context);
29
+ // 1. Dump UI hierarchy
30
+ const elements = await robot.getElementsOnScreen();
31
+ if (elements.length === 0) {
32
+ return {
33
+ tier: this.name,
34
+ status: "FALLBACK",
35
+ fallbackHint: "UI hierarchy dump returned zero elements",
36
+ };
37
+ }
38
+ // 2. If there's a tap target, find and tap it
39
+ const tapTarget = context.step.tapTarget;
40
+ if (tapTarget) {
41
+ if (tapTarget.resourceId) {
42
+ const target = elements.find(el => el.identifier === tapTarget.resourceId);
43
+ if (target) {
44
+ const centerX = target.rect.x + Math.floor(target.rect.width / 2);
45
+ const centerY = target.rect.y + Math.floor(target.rect.height / 2);
46
+ await robot.tap(centerX, centerY);
47
+ return {
48
+ tier: this.name,
49
+ status: "SUCCESS",
50
+ observation: `Tapped ${tapTarget.resourceId} at (${centerX}, ${centerY})`,
51
+ rawData: JSON.stringify({ target, elementsCount: elements.length }),
52
+ };
53
+ }
54
+ // resource-id not found
55
+ return {
56
+ tier: this.name,
57
+ status: "FAIL",
58
+ observation: `Element with resource-id "${tapTarget.resourceId}" not found in ${elements.length} elements`,
59
+ verification: {
60
+ passed: false,
61
+ expected: `resource-id: ${tapTarget.resourceId}`,
62
+ actual: `Not found. Available IDs: ${elements.filter(e => e.identifier).map(e => e.identifier).slice(0, 10).join(", ")}`,
63
+ },
64
+ rawData: JSON.stringify({ elementsCount: elements.length }),
65
+ };
66
+ }
67
+ if (tapTarget.coordinates) {
68
+ await robot.tap(tapTarget.coordinates.x, tapTarget.coordinates.y);
69
+ return {
70
+ tier: this.name,
71
+ status: "SUCCESS",
72
+ observation: `Tapped coordinates (${tapTarget.coordinates.x}, ${tapTarget.coordinates.y})`,
73
+ };
74
+ }
75
+ }
76
+ // 3. No tap target — return UI hierarchy as observation
77
+ const summary = elements.slice(0, 20).map(el => ({
78
+ type: el.type,
79
+ text: el.text,
80
+ id: el.identifier,
81
+ }));
82
+ return {
83
+ tier: this.name,
84
+ status: "SUCCESS",
85
+ observation: `UI hierarchy: ${elements.length} elements found`,
86
+ rawData: JSON.stringify(summary),
87
+ };
88
+ }
89
+ }
90
+ exports.UiAutomatorTier = UiAutomatorTier;
91
+ //# sourceMappingURL=uiautomator-tier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uiautomator-tier.js","sourceRoot":"","sources":["../../src/tiers/uiautomator-tier.ts"],"names":[],"mappings":"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"}
package/lib/utils.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function validatePackageName(packageName: string): void;
2
+ export declare function validateLocale(locale: string): void;
3
+ export declare function validateFileExtension(filePath: string, allowedExtensions: string[], toolName: string): void;
4
+ export declare function validateOutputPath(filePath: string): void;
package/lib/utils.js ADDED
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validatePackageName = validatePackageName;
7
+ exports.validateLocale = validateLocale;
8
+ exports.validateFileExtension = validateFileExtension;
9
+ exports.validateOutputPath = validateOutputPath;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_os_1 = __importDefault(require("node:os"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const robot_1 = require("./robot");
14
+ function validatePackageName(packageName) {
15
+ if (!/^[a-zA-Z0-9._]+$/.test(packageName)) {
16
+ throw new robot_1.ActionableError(`Invalid package name: "${packageName}"`);
17
+ }
18
+ }
19
+ function validateLocale(locale) {
20
+ if (!/^[a-zA-Z0-9,\- ]+$/.test(locale)) {
21
+ throw new robot_1.ActionableError(`Invalid locale: "${locale}"`);
22
+ }
23
+ }
24
+ function getAllowedRoots() {
25
+ const roots = [
26
+ node_os_1.default.tmpdir(),
27
+ process.cwd(),
28
+ ];
29
+ // macOS /tmp is a symlink to /private/tmp, add both to be safe
30
+ if (process.platform === "darwin") {
31
+ roots.push("/tmp");
32
+ roots.push("/private/tmp");
33
+ }
34
+ return roots.map(r => node_path_1.default.resolve(r));
35
+ }
36
+ function isPathUnderRoot(filePath, root) {
37
+ const relative = node_path_1.default.relative(root, filePath);
38
+ if (relative === "") {
39
+ return false;
40
+ }
41
+ if (node_path_1.default.isAbsolute(relative)) {
42
+ return false;
43
+ }
44
+ if (relative.startsWith("..")) {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ function validateFileExtension(filePath, allowedExtensions, toolName) {
50
+ const ext = node_path_1.default.extname(filePath).toLowerCase();
51
+ if (!allowedExtensions.includes(ext)) {
52
+ throw new robot_1.ActionableError(`${toolName} requires a ${allowedExtensions.join(", ")} file extension, got: "${ext || "(none)"}"`);
53
+ }
54
+ }
55
+ function resolveWithSymlinks(filePath) {
56
+ const resolved = node_path_1.default.resolve(filePath);
57
+ const dir = node_path_1.default.dirname(resolved);
58
+ const filename = node_path_1.default.basename(resolved);
59
+ try {
60
+ return node_path_1.default.join(node_fs_1.default.realpathSync(dir), filename);
61
+ }
62
+ catch {
63
+ return resolved;
64
+ }
65
+ }
66
+ function validateOutputPath(filePath) {
67
+ const resolved = resolveWithSymlinks(filePath);
68
+ const allowedRoots = getAllowedRoots();
69
+ const isWindows = process.platform === "win32";
70
+ const isAllowed = allowedRoots.some(root => {
71
+ if (isWindows) {
72
+ return isPathUnderRoot(resolved.toLowerCase(), root.toLowerCase());
73
+ }
74
+ return isPathUnderRoot(resolved, root);
75
+ });
76
+ if (!isAllowed) {
77
+ const dir = node_path_1.default.dirname(resolved);
78
+ throw new robot_1.ActionableError(`"${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.`);
79
+ }
80
+ }
81
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { SwipeDirection, ScreenSize, ScreenElement, Orientation } from "./robot";
2
+ export interface SourceTreeElementRect {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface SourceTreeElement {
9
+ type: string;
10
+ label?: string;
11
+ name?: string;
12
+ value?: string;
13
+ rawIdentifier?: string;
14
+ rect: SourceTreeElementRect;
15
+ isVisible?: string;
16
+ children?: Array<SourceTreeElement>;
17
+ }
18
+ export interface SourceTree {
19
+ value: SourceTreeElement;
20
+ }
21
+ export declare class WebDriverAgent {
22
+ private readonly host;
23
+ private readonly port;
24
+ constructor(host: string, port: number);
25
+ isRunning(): Promise<boolean>;
26
+ createSession(): Promise<string>;
27
+ deleteSession(sessionId: string): Promise<any>;
28
+ withinSession(fn: (url: string) => Promise<any>): Promise<any>;
29
+ getScreenSize(sessionUrl?: string): Promise<ScreenSize>;
30
+ sendKeys(keys: string): Promise<void>;
31
+ pressButton(button: string): Promise<void>;
32
+ tap(x: number, y: number): Promise<void>;
33
+ doubleTap(x: number, y: number): Promise<void>;
34
+ longPress(x: number, y: number, duration: number): Promise<void>;
35
+ private isVisible;
36
+ private filterSourceElements;
37
+ getPageSource(): Promise<SourceTree>;
38
+ getElementsOnScreen(): Promise<ScreenElement[]>;
39
+ openUrl(url: string): Promise<void>;
40
+ getScreenshot(): Promise<Buffer>;
41
+ swipe(direction: SwipeDirection): Promise<void>;
42
+ swipeFromCoordinate(x: number, y: number, direction: SwipeDirection, distance?: number): Promise<void>;
43
+ setOrientation(orientation: Orientation): Promise<void>;
44
+ getOrientation(): Promise<Orientation>;
45
+ }
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebDriverAgent = void 0;
4
+ const robot_1 = require("./robot");
5
+ class WebDriverAgent {
6
+ host;
7
+ port;
8
+ constructor(host, port) {
9
+ this.host = host;
10
+ this.port = port;
11
+ }
12
+ async isRunning() {
13
+ const url = `http://${this.host}:${this.port}/status`;
14
+ try {
15
+ const response = await fetch(url);
16
+ const json = await response.json();
17
+ return response.status === 200 && json.value?.ready === true;
18
+ }
19
+ catch (error) {
20
+ // console.error(`Failed to connect to WebDriverAgent: ${error}`);
21
+ return false;
22
+ }
23
+ }
24
+ async createSession() {
25
+ const url = `http://${this.host}:${this.port}/session`;
26
+ const response = await fetch(url, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ },
31
+ body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }),
32
+ });
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ throw new robot_1.ActionableError(`Failed to create WebDriver session: ${response.status} ${errorText}`);
36
+ }
37
+ const json = await response.json();
38
+ if (!json.value || !json.value.sessionId) {
39
+ throw new robot_1.ActionableError(`Invalid session response: ${JSON.stringify(json)}`);
40
+ }
41
+ return json.value.sessionId;
42
+ }
43
+ async deleteSession(sessionId) {
44
+ const url = `http://${this.host}:${this.port}/session/${sessionId}`;
45
+ const response = await fetch(url, { method: "DELETE" });
46
+ return response.json();
47
+ }
48
+ async withinSession(fn) {
49
+ const sessionId = await this.createSession();
50
+ const url = `http://${this.host}:${this.port}/session/${sessionId}`;
51
+ const result = await fn(url);
52
+ await this.deleteSession(sessionId);
53
+ return result;
54
+ }
55
+ async getScreenSize(sessionUrl) {
56
+ if (sessionUrl) {
57
+ const url = `${sessionUrl}/wda/screen`;
58
+ const response = await fetch(url);
59
+ const json = await response.json();
60
+ return {
61
+ width: json.value.screenSize.width,
62
+ height: json.value.screenSize.height,
63
+ scale: json.value.scale || 1,
64
+ };
65
+ }
66
+ else {
67
+ return this.withinSession(async (sessionUrlInner) => {
68
+ const url = `${sessionUrlInner}/wda/screen`;
69
+ const response = await fetch(url);
70
+ const json = await response.json();
71
+ return {
72
+ width: json.value.screenSize.width,
73
+ height: json.value.screenSize.height,
74
+ scale: json.value.scale || 1,
75
+ };
76
+ });
77
+ }
78
+ }
79
+ async sendKeys(keys) {
80
+ await this.withinSession(async (sessionUrl) => {
81
+ const url = `${sessionUrl}/wda/keys`;
82
+ await fetch(url, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ },
87
+ body: JSON.stringify({ value: [keys] }),
88
+ });
89
+ });
90
+ }
91
+ async pressButton(button) {
92
+ const _map = {
93
+ "HOME": "home",
94
+ "VOLUME_UP": "volumeup",
95
+ "VOLUME_DOWN": "volumedown",
96
+ };
97
+ if (button === "ENTER") {
98
+ await this.sendKeys("\n");
99
+ return;
100
+ }
101
+ // Type assertion to check if button is a key of _map
102
+ if (!(button in _map)) {
103
+ throw new robot_1.ActionableError(`Button "${button}" is not supported`);
104
+ }
105
+ await this.withinSession(async (sessionUrl) => {
106
+ const url = `${sessionUrl}/wda/pressButton`;
107
+ const response = await fetch(url, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ },
112
+ body: JSON.stringify({
113
+ name: button,
114
+ }),
115
+ });
116
+ return response.json();
117
+ });
118
+ }
119
+ async tap(x, y) {
120
+ await this.withinSession(async (sessionUrl) => {
121
+ const url = `${sessionUrl}/actions`;
122
+ await fetch(url, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ },
127
+ body: JSON.stringify({
128
+ actions: [
129
+ {
130
+ type: "pointer",
131
+ id: "finger1",
132
+ parameters: { pointerType: "touch" },
133
+ actions: [
134
+ { type: "pointerMove", duration: 0, x, y },
135
+ { type: "pointerDown", button: 0 },
136
+ { type: "pause", duration: 100 },
137
+ { type: "pointerUp", button: 0 }
138
+ ]
139
+ }
140
+ ]
141
+ }),
142
+ });
143
+ });
144
+ }
145
+ async doubleTap(x, y) {
146
+ await this.withinSession(async (sessionUrl) => {
147
+ const url = `${sessionUrl}/actions`;
148
+ await fetch(url, {
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/json",
152
+ },
153
+ body: JSON.stringify({
154
+ actions: [
155
+ {
156
+ type: "pointer",
157
+ id: "finger1",
158
+ parameters: { pointerType: "touch" },
159
+ actions: [
160
+ { type: "pointerMove", duration: 0, x, y },
161
+ { type: "pointerDown", button: 0 },
162
+ { type: "pause", duration: 50 },
163
+ { type: "pointerUp", button: 0 },
164
+ { type: "pause", duration: 100 },
165
+ { type: "pointerDown", button: 0 },
166
+ { type: "pause", duration: 50 },
167
+ { type: "pointerUp", button: 0 }
168
+ ]
169
+ }
170
+ ]
171
+ }),
172
+ });
173
+ });
174
+ }
175
+ async longPress(x, y, duration) {
176
+ await this.withinSession(async (sessionUrl) => {
177
+ const url = `${sessionUrl}/actions`;
178
+ await fetch(url, {
179
+ method: "POST",
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ },
183
+ body: JSON.stringify({
184
+ actions: [
185
+ {
186
+ type: "pointer",
187
+ id: "finger1",
188
+ parameters: { pointerType: "touch" },
189
+ actions: [
190
+ { type: "pointerMove", duration: 0, x, y },
191
+ { type: "pointerDown", button: 0 },
192
+ { type: "pause", duration },
193
+ { type: "pointerUp", button: 0 }
194
+ ]
195
+ }
196
+ ]
197
+ }),
198
+ });
199
+ });
200
+ }
201
+ isVisible(rect) {
202
+ return rect.x >= 0 && rect.y >= 0;
203
+ }
204
+ filterSourceElements(source) {
205
+ const output = [];
206
+ const acceptedTypes = ["TextField", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image"];
207
+ if (acceptedTypes.includes(source.type)) {
208
+ if (source.isVisible === "1" && this.isVisible(source.rect)) {
209
+ if (source.label !== null || source.name !== null || source.rawIdentifier !== null) {
210
+ output.push({
211
+ type: source.type,
212
+ label: source.label,
213
+ name: source.name,
214
+ value: source.value,
215
+ identifier: source.rawIdentifier,
216
+ rect: {
217
+ x: source.rect.x,
218
+ y: source.rect.y,
219
+ width: source.rect.width,
220
+ height: source.rect.height,
221
+ },
222
+ });
223
+ }
224
+ }
225
+ }
226
+ if (source.children) {
227
+ for (const child of source.children) {
228
+ output.push(...this.filterSourceElements(child));
229
+ }
230
+ }
231
+ return output;
232
+ }
233
+ async getPageSource() {
234
+ const url = `http://${this.host}:${this.port}/source/?format=json`;
235
+ const response = await fetch(url);
236
+ const json = await response.json();
237
+ return json;
238
+ }
239
+ async getElementsOnScreen() {
240
+ const source = await this.getPageSource();
241
+ return this.filterSourceElements(source.value);
242
+ }
243
+ async openUrl(url) {
244
+ await this.withinSession(async (sessionUrl) => {
245
+ await fetch(`${sessionUrl}/url`, {
246
+ method: "POST",
247
+ body: JSON.stringify({ url }),
248
+ });
249
+ });
250
+ }
251
+ async getScreenshot() {
252
+ const url = `http://${this.host}:${this.port}/screenshot`;
253
+ const response = await fetch(url);
254
+ const json = await response.json();
255
+ return Buffer.from(json.value, "base64");
256
+ }
257
+ async swipe(direction) {
258
+ await this.withinSession(async (sessionUrl) => {
259
+ const screenSize = await this.getScreenSize(sessionUrl);
260
+ let x0, y0, x1, y1;
261
+ // Use 60% of the width/height for swipe distance
262
+ const verticalDistance = Math.floor(screenSize.height * 0.6);
263
+ const horizontalDistance = Math.floor(screenSize.width * 0.6);
264
+ const centerX = Math.floor(screenSize.width / 2);
265
+ const centerY = Math.floor(screenSize.height / 2);
266
+ switch (direction) {
267
+ case "up":
268
+ x0 = x1 = centerX;
269
+ y0 = centerY + Math.floor(verticalDistance / 2);
270
+ y1 = centerY - Math.floor(verticalDistance / 2);
271
+ break;
272
+ case "down":
273
+ x0 = x1 = centerX;
274
+ y0 = centerY - Math.floor(verticalDistance / 2);
275
+ y1 = centerY + Math.floor(verticalDistance / 2);
276
+ break;
277
+ case "left":
278
+ y0 = y1 = centerY;
279
+ x0 = centerX + Math.floor(horizontalDistance / 2);
280
+ x1 = centerX - Math.floor(horizontalDistance / 2);
281
+ break;
282
+ case "right":
283
+ y0 = y1 = centerY;
284
+ x0 = centerX - Math.floor(horizontalDistance / 2);
285
+ x1 = centerX + Math.floor(horizontalDistance / 2);
286
+ break;
287
+ default:
288
+ throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
289
+ }
290
+ const url = `${sessionUrl}/actions`;
291
+ const response = await fetch(url, {
292
+ method: "POST",
293
+ headers: {
294
+ "Content-Type": "application/json",
295
+ },
296
+ body: JSON.stringify({
297
+ actions: [
298
+ {
299
+ type: "pointer",
300
+ id: "finger1",
301
+ parameters: { pointerType: "touch" },
302
+ actions: [
303
+ { type: "pointerMove", duration: 0, x: x0, y: y0 },
304
+ { type: "pointerDown", button: 0 },
305
+ { type: "pointerMove", duration: 1000, x: x1, y: y1 },
306
+ { type: "pointerUp", button: 0 }
307
+ ]
308
+ }
309
+ ]
310
+ }),
311
+ });
312
+ if (!response.ok) {
313
+ const errorText = await response.text();
314
+ throw new robot_1.ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`);
315
+ }
316
+ // Clear actions to ensure they complete
317
+ await fetch(`${sessionUrl}/actions`, {
318
+ method: "DELETE",
319
+ });
320
+ });
321
+ }
322
+ async swipeFromCoordinate(x, y, direction, distance = 400) {
323
+ await this.withinSession(async (sessionUrl) => {
324
+ // Use simple coordinates like the working swipe method
325
+ const x0 = x;
326
+ const y0 = y;
327
+ let x1 = x;
328
+ let y1 = y;
329
+ // Calculate target position based on direction and distance
330
+ switch (direction) {
331
+ case "up":
332
+ y1 = y - distance; // Move up by specified distance
333
+ break;
334
+ case "down":
335
+ y1 = y + distance; // Move down by specified distance
336
+ break;
337
+ case "left":
338
+ x1 = x - distance; // Move left by specified distance
339
+ break;
340
+ case "right":
341
+ x1 = x + distance; // Move right by specified distance
342
+ break;
343
+ default:
344
+ throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
345
+ }
346
+ const url = `${sessionUrl}/actions`;
347
+ const response = await fetch(url, {
348
+ method: "POST",
349
+ headers: {
350
+ "Content-Type": "application/json",
351
+ },
352
+ body: JSON.stringify({
353
+ actions: [
354
+ {
355
+ type: "pointer",
356
+ id: "finger1",
357
+ parameters: { pointerType: "touch" },
358
+ actions: [
359
+ { type: "pointerMove", duration: 0, x: x0, y: y0 },
360
+ { type: "pointerDown", button: 0 },
361
+ { type: "pointerMove", duration: 1000, x: x1, y: y1 },
362
+ { type: "pointerUp", button: 0 }
363
+ ]
364
+ }
365
+ ]
366
+ }),
367
+ });
368
+ if (!response.ok) {
369
+ const errorText = await response.text();
370
+ throw new robot_1.ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`);
371
+ }
372
+ // Clear actions to ensure they complete
373
+ await fetch(`${sessionUrl}/actions`, {
374
+ method: "DELETE",
375
+ });
376
+ });
377
+ }
378
+ async setOrientation(orientation) {
379
+ await this.withinSession(async (sessionUrl) => {
380
+ const url = `${sessionUrl}/orientation`;
381
+ await fetch(url, {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify({
385
+ orientation: orientation.toUpperCase()
386
+ })
387
+ });
388
+ });
389
+ }
390
+ async getOrientation() {
391
+ return this.withinSession(async (sessionUrl) => {
392
+ const url = `${sessionUrl}/orientation`;
393
+ const response = await fetch(url);
394
+ const json = await response.json();
395
+ return json.value.toLowerCase();
396
+ });
397
+ }
398
+ }
399
+ exports.WebDriverAgent = WebDriverAgent;
400
+ //# sourceMappingURL=webdriver-agent.js.map