diff --git a/cli/io.go b/cli/io.go index f2e9ef4..16bec98 100644 --- a/cli/io.go +++ b/cli/io.go @@ -131,6 +131,48 @@ var ioTextCmd = &cobra.Command{ }, } +var ioSwipeCmd = &cobra.Command{ + Use: "swipe [x1,y1,x2,y2]", + Short: "Swipe on a device screen from one point to another", + Long: `Sends a swipe gesture to the specified device from coordinates x1,y1 to x2,y2. Coordinates should be provided as a single string "x1,y1,x2,y2".`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + coordsStr := args[0] + parts := strings.Split(coordsStr, ",") + if len(parts) != 4 { + response := commands.NewErrorResponse(fmt.Errorf("invalid coordinate format. Expected 'x1,y1,x2,y2', got '%s'", coordsStr)) + printJson(response) + return fmt.Errorf("%s", response.Error) + } + + x1, errX1 := strconv.Atoi(strings.TrimSpace(parts[0])) + y1, errY1 := strconv.Atoi(strings.TrimSpace(parts[1])) + x2, errX2 := strconv.Atoi(strings.TrimSpace(parts[2])) + y2, errY2 := strconv.Atoi(strings.TrimSpace(parts[3])) + + if errX1 != nil || errY1 != nil || errX2 != nil || errY2 != nil { + response := commands.NewErrorResponse(fmt.Errorf("invalid coordinate values. x1, y1, x2, y2 must be integers. Got x1='%s', y1='%s', x2='%s', y2='%s'", parts[0], parts[1], parts[2], parts[3])) + printJson(response) + return fmt.Errorf("%s", response.Error) + } + + req := commands.SwipeRequest{ + DeviceID: deviceId, + X1: x1, + Y1: y1, + X2: x2, + Y2: y2, + } + + response := commands.SwipeCommand(req) + printJson(response) + if response.Status == "error" { + return fmt.Errorf("%s", response.Error) + } + return nil + }, +} + func init() { rootCmd.AddCommand(ioCmd) @@ -139,10 +181,12 @@ func init() { ioCmd.AddCommand(ioLongPressCmd) ioCmd.AddCommand(ioButtonCmd) ioCmd.AddCommand(ioTextCmd) + ioCmd.AddCommand(ioSwipeCmd) // io command flags ioTapCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to tap on") ioLongPressCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to long press on") ioButtonCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to press button on") ioTextCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to send keys to") + ioSwipeCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to swipe on") } diff --git a/commands/input.go b/commands/input.go index 475b3f0..1b5acca 100644 --- a/commands/input.go +++ b/commands/input.go @@ -39,6 +39,15 @@ type GestureRequest struct { Actions []interface{} `json:"actions"` } +// SwipeRequest represents the parameters for a swipe command +type SwipeRequest struct { + DeviceID string `json:"deviceId"` + X1 int `json:"x1"` + Y1 int `json:"y1"` + X2 int `json:"x2"` + Y2 int `json:"y2"` +} + // TapCommand performs a tap operation on the specified device func TapCommand(req TapRequest) *CommandResponse { if req.X < 0 || req.Y < 0 { @@ -183,3 +192,29 @@ func GestureCommand(req GestureRequest) *CommandResponse { "message": fmt.Sprintf("Performed gesture on device %s with %d actions", targetDevice.ID(), len(req.Actions)), }) } + +// SwipeCommand performs a swipe operation on the specified device +func SwipeCommand(req SwipeRequest) *CommandResponse { + if req.X1 < 0 || req.Y1 < 0 || req.X2 < 0 || req.Y2 < 0 { + return NewErrorResponse(fmt.Errorf("all coordinates must be non-negative, got x1=%d, y1=%d, x2=%d, y2=%d", req.X1, req.Y1, req.X2, req.Y2)) + } + + targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID) + if err != nil { + return NewErrorResponse(fmt.Errorf("error finding device: %v", err)) + } + + err = targetDevice.StartAgent() + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to start agent on device %s: %v", targetDevice.ID(), err)) + } + + err = targetDevice.Swipe(req.X1, req.Y1, req.X2, req.Y2) + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to swipe on device %s: %v", targetDevice.ID(), err)) + } + + return NewSuccessResponse(map[string]interface{}{ + "message": fmt.Sprintf("Swiped on device %s from (%d,%d) to (%d,%d)", targetDevice.ID(), req.X1, req.Y1, req.X2, req.Y2), + }) +} diff --git a/devices/android.go b/devices/android.go index 4ee9d25..cf1fd0c 100644 --- a/devices/android.go +++ b/devices/android.go @@ -129,6 +129,16 @@ func (d AndroidDevice) LongPress(x, y int) error { return nil } +// Swipe simulates a swipe gesture from (x1, y1) to (x2, y2) on the Android device with 1000ms duration. +func (d AndroidDevice) Swipe(x1, y1, x2, y2 int) error { + _, err := d.runAdbCommand("shell", "input", "swipe", fmt.Sprintf("%d", x1), fmt.Sprintf("%d", y1), fmt.Sprintf("%d", x2), fmt.Sprintf("%d", y2), "1000") + if err != nil { + return err + } + + return nil +} + // Gesture performs a sequence of touch actions on the Android device func (d AndroidDevice) Gesture(actions []wda.TapAction) error { diff --git a/devices/common.go b/devices/common.go index af57b85..0162e76 100644 --- a/devices/common.go +++ b/devices/common.go @@ -31,6 +31,7 @@ type ControllableDevice interface { Reboot() error Tap(x, y int) error LongPress(x, y int) error + Swipe(x1, y1, x2, y2 int) error Gesture(actions []wda.TapAction) error StartAgent() error SendKeys(text string) error diff --git a/devices/ios.go b/devices/ios.go index df8d8bb..48f35a8 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -128,6 +128,10 @@ func (d IOSDevice) LongPress(x, y int) error { return d.wdaClient.LongPress(x, y) } +func (d IOSDevice) Swipe(x1, y1, x2, y2 int) error { + return d.wdaClient.Swipe(x1, y1, x2, y2) +} + func (d IOSDevice) Gesture(actions []wda.TapAction) error { return d.wdaClient.Gesture(actions) } diff --git a/devices/simulator.go b/devices/simulator.go index 1f42683..7ce0280 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -414,6 +414,10 @@ func (s SimulatorDevice) LongPress(x, y int) error { return s.wdaClient.LongPress(x, y) } +func (s SimulatorDevice) Swipe(x1, y1, x2, y2 int) error { + return s.wdaClient.Swipe(x1, y1, x2, y2) +} + func (s SimulatorDevice) Gesture(actions []wda.TapAction) error { return s.wdaClient.Gesture(actions) } diff --git a/devices/wda/swipe.go b/devices/wda/swipe.go new file mode 100644 index 0000000..3540fb7 --- /dev/null +++ b/devices/wda/swipe.go @@ -0,0 +1,39 @@ +package wda + +import ( + "fmt" +) + +func (c *WdaClient) Swipe(x1, y1, x2, y2 int) error { + + sessionId, err := c.CreateSession() + if err != nil { + return err + } + + defer c.DeleteSession(sessionId) + + data := ActionsRequest{ + Actions: []Pointer{ + { + Type: "pointer", + ID: "finger1", + Parameters: PointerParameters{ + PointerType: "touch", + }, + Actions: []TapAction{ + {Type: "pointerMove", Duration: 0, X: x1, Y: y1}, + {Type: "pointerDown", Button: 0}, + {Type: "pointerMove", Duration: 1000, X: x2, Y: y2}, + {Type: "pointerUp", Button: 0}, + }, + }, + }, + } + + _, err = c.PostEndpoint(fmt.Sprintf("session/%s/actions", sessionId), data) + if err != nil { + return err + } + return nil +} diff --git a/server/server.go b/server/server.go index 90e4c95..206f307 100644 --- a/server/server.go +++ b/server/server.go @@ -158,6 +158,8 @@ func handleJSONRPC(w http.ResponseWriter, r *http.Request) { result, err = handleIoText(req.Params) case "io_button": result, err = handleIoButton(req.Params) + case "io_swipe": + result, err = handleIoSwipe(req.Params) case "io_gesture": result, err = handleIoGesture(req.Params) case "url": @@ -245,6 +247,14 @@ type IoLongPressParams struct { Y int `json:"y"` } +type IoSwipeParams struct { + DeviceID string `json:"deviceId"` + X1 int `json:"x1"` + Y1 int `json:"y1"` + X2 int `json:"x2"` + Y2 int `json:"y2"` +} + func handleIoTap(params json.RawMessage) (interface{}, error) { if len(params) == 0 { return nil, fmt.Errorf("'params' is required with fields: deviceId, x, y") @@ -293,6 +303,49 @@ func handleIoLongPress(params json.RawMessage) (interface{}, error) { return okResponse, nil } +func handleIoSwipe(params json.RawMessage) (interface{}, error) { + if len(params) == 0 { + return nil, fmt.Errorf("'params' is required with fields: deviceId, x1, y1, x2, y2") + } + + var ioSwipeParams IoSwipeParams + if err := json.Unmarshal(params, &ioSwipeParams); err != nil { + return nil, fmt.Errorf("invalid parameters: %v. Expected fields: deviceId, x1, y1, x2, y2", err) + } + + if ioSwipeParams.DeviceID == "" { + return nil, fmt.Errorf("'deviceId' is required") + } + + // validate that coordinates are provided (x1,y1,x2,y2 must be present) + var rawParams map[string]interface{} + if err := json.Unmarshal(params, &rawParams); err != nil { + return nil, fmt.Errorf("invalid parameters format") + } + + requiredFields := []string{"x1", "y1", "x2", "y2"} + for _, field := range requiredFields { + if _, exists := rawParams[field]; !exists { + return nil, fmt.Errorf("'%s' is required", field) + } + } + + req := commands.SwipeRequest{ + DeviceID: ioSwipeParams.DeviceID, + X1: ioSwipeParams.X1, + Y1: ioSwipeParams.Y1, + X2: ioSwipeParams.X2, + Y2: ioSwipeParams.Y2, + } + + response := commands.SwipeCommand(req) + if response.Status == "error" { + return nil, fmt.Errorf("%s", response.Error) + } + + return okResponse, nil +} + type IoTextParams struct { DeviceID string `json:"deviceId"` Text string `json:"text"`