refactor: remove unused handlers and related files to streamline the server codebase

This commit is contained in:
lilong.129
2025-06-21 21:56:24 +08:00
parent c802327e39
commit a1c8b7fab3
14 changed files with 159 additions and 1042 deletions

View File

@@ -1 +1 @@
v5.0.0-beta-2506211542
v5.0.0-beta-2506212208

View File

@@ -1,132 +0,0 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func (r *Router) foregroundAppHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
appInfo, err := driver.ForegroundInfo()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, appInfo)
}
func (r *Router) appInfoHandler(c *gin.Context) {
var req option.ActionOptions
if err := c.ShouldBindQuery(&req); err != nil {
RenderErrorValidateRequest(c, err)
return
}
// Set platform and serial from URL parameters
setRequestContextFromURL(c, &req)
// Validate for HTTP API usage
if err := req.ValidateForHTTPAPI(option.ACTION_AppInfo); err != nil {
RenderErrorValidateRequest(c, err)
return
}
device, err := r.GetDevice(c)
if err != nil {
return
}
if androidDevice, ok := device.(*uixt.AndroidDevice); ok {
appInfo, err := androidDevice.GetAppInfo(req.PackageName)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, appInfo)
return
} else if iOSDevice, ok := device.(*uixt.IOSDevice); ok {
appInfo, err := iOSDevice.GetAppInfo(req.PackageName)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, appInfo)
return
}
}
func (r *Router) clearAppHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_AppClear)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.AppClear(req.PackageName)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) launchAppHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_AppLaunch)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.AppLaunch(req.PackageName)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) terminalAppHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_AppTerminate)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
_, err = driver.AppTerminate(req.PackageName)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) uninstallAppHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_AppUninstall)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.GetDevice().Uninstall(req.PackageName)
if err != nil {
log.Err(err).Msg("failed to uninstall app")
}
RenderSuccess(c, true)
}

View File

@@ -3,6 +3,8 @@ package server
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
@@ -12,10 +14,84 @@ import (
"github.com/httprunner/httprunner/v5/uixt/option"
)
func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) {
var device uixt.IDevice
var driver uixt.IDriver
func (r *Router) GetDevice(c *gin.Context) (uixt.IDevice, error) {
platform := c.Param("platform")
switch strings.ToLower(platform) {
case "android":
serial := c.Param("serial")
if serial == "" {
err := fmt.Errorf("[%s]: serial is empty", c.HandlerName())
log.Error().Err(err).Str("platform", platform).Msg(err.Error())
RenderError(c, err)
return nil, err
}
device, err := uixt.NewAndroidDevice(option.WithSerialNumber(serial))
if err != nil {
time.Sleep(5 * time.Second)
device, err = uixt.NewAndroidDevice(option.WithSerialNumber(serial))
if err != nil {
log.Error().Err(err).Str("platform", platform).Str("serial", serial).
Msg(fmt.Sprintf("[%s]: Device Not Found; %s", c.HandlerName(), err.Error()))
RenderErrorInitDevice(c, err)
return nil, err
}
}
c.Set("device", device)
return device, nil
case "ios":
serial := c.Param("serial")
if serial == "" {
err := fmt.Errorf("[%s]: serial is empty", c.HandlerName())
log.Error().Err(err).Str("platform", platform).Msg(err.Error())
RenderError(c, err)
return nil, err
}
device, err := uixt.NewIOSDevice(
option.WithUDID(serial),
option.WithWDAPort(8700),
option.WithWDAMjpegPort(8800),
option.WithResetHomeOnStartup(false))
if err != nil {
log.Error().Err(err).Str("platform", platform).Str("serial", serial).
Msg(fmt.Sprintf("[%s]: Device Not Found", c.HandlerName()))
RenderErrorInitDevice(c, err)
return nil, err
}
c.Set("device", device)
return device, nil
case "browser":
serial := c.Param("serial")
if serial == "" {
err := fmt.Errorf("[%s]: serial is empty", c.HandlerName())
log.Error().Err(err).Str("platform", platform).Msg(err.Error())
RenderError(c, err)
return nil, err
}
device, err := uixt.NewBrowserDevice(option.WithBrowserID(serial))
if err != nil {
RenderErrorInitDevice(c, err)
return nil, err
}
c.Set("device", device)
return device, nil
default:
err := fmt.Errorf("[%s]: invalid platform", c.HandlerName())
RenderError(c, err)
return nil, err
}
}
func (r *Router) GetDriver(c *gin.Context) (*uixt.XTDriver, error) {
platform := c.Param("platform")
// Try to get existing device from context
deviceObj, exists := c.Get("device")
var device uixt.IDevice
var err error
if !exists {
device, err = r.GetDevice(c)
if err != nil {
@@ -25,32 +101,24 @@ func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error)
device = deviceObj.(uixt.IDevice)
}
driver, err = device.NewDriver()
// Create driver
driver, err := device.NewDriver()
if err != nil {
log.Error().Err(err).Str("platform", platform).Str("serial", device.UUID()).
Msg(fmt.Sprintf("[%s]: Failed New Driver", c.HandlerName()))
RenderErrorInitDriver(c, err)
return
return nil, err
}
driverExt, err = uixt.NewXTDriver(driver,
option.WithCVService(option.CVServiceTypeVEDEM))
// Create XTDriver wrapper
xtDriver, err := uixt.NewXTDriver(driver)
if err != nil {
RenderErrorInitDriver(c, err)
return
return nil, err
}
c.Set("driver", driverExt)
return driverExt, nil
}
func (r *Router) GetDevice(c *gin.Context) (device uixt.IDevice, err error) {
platform := c.Param("platform")
serial := c.Param("serial")
device, err = uixt.NewDeviceWithDefault(platform, serial)
if err != nil {
RenderErrorInitDriver(c, err)
return
}
c.Set("device", device)
return device, nil
c.Set("driver", xtDriver)
return xtDriver, nil
}
func RenderSuccess(c *gin.Context, result interface{}) {
@@ -87,6 +155,21 @@ func RenderErrorInitDriver(c *gin.Context, err error) {
c.Abort()
}
func RenderErrorInitDevice(c *gin.Context, err error) {
log.Error().Err(err).Msg("init device failed")
errCode := code.GetErrorCode(err)
if errCode == code.GeneralFail {
errCode = code.GetErrorCode(code.DeviceConnectionError)
}
c.JSON(http.StatusInternalServerError,
HttpResponse{
Code: errCode,
Message: "grey init device failed",
},
)
c.Abort()
}
func RenderErrorValidateRequest(c *gin.Context, err error) {
log.Error().Err(err).Msg("validate request failed")
c.JSON(http.StatusBadRequest, HttpResponse{

View File

@@ -1,169 +0,0 @@
package server
import (
"os"
"path"
"github.com/Masterminds/semver"
"github.com/danielpaulus/go-ios/ios"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/pkg/gadb"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func (r *Router) listDeviceHandler(c *gin.Context) {
var deviceList []interface{}
client, err := gadb.NewClient()
if err == nil {
androidDevices, err := client.DeviceList()
if err == nil {
for _, device := range androidDevices {
brand, err := device.Brand()
if err != nil {
RenderError(c, err)
return
}
model, err := device.Model()
if err != nil {
RenderError(c, err)
return
}
version, err := device.SdkVersion()
if err != nil {
RenderError(c, err)
return
}
deviceInfo := map[string]interface{}{
"serial": device.Serial(),
"brand": brand,
"model": model,
"version": version,
"platform": "android",
}
deviceList = append(deviceList, deviceInfo)
}
}
}
iosDevices, err := ios.ListDevices()
if err == nil {
for _, dev := range iosDevices.DeviceList {
device, err := uixt.NewIOSDevice(
option.WithUDID(dev.Properties.SerialNumber))
if err != nil {
continue
}
properties := device.Properties
err = ios.Pair(dev)
if err != nil {
log.Error().Err(err).Msg("failed to pair device")
continue
}
version, err := ios.GetProductVersion(dev)
if err != nil {
continue
}
if version.LessThan(semver.MustParse("17.4.0")) &&
version.GreaterThan(ios.IOS17()) {
log.Warn().Msg("not support ios 17.0-17.3")
continue
}
plist, err := ios.GetValuesPlist(dev)
if err != nil {
log.Error().Err(err).Msg("failed to get device info")
continue
}
deviceInfo := map[string]interface{}{
"udid": properties.SerialNumber,
"platform": "ios",
"brand": "apple",
"model": plist["ProductType"],
"version": plist["ProductVersion"],
}
deviceList = append(deviceList, deviceInfo)
}
}
RenderSuccess(c, deviceList)
}
func createBrowserHandler(c *gin.Context) {
var createBrowserReq CreateBrowserRequest
if err := c.ShouldBindJSON(&createBrowserReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
browserInfo, err := uixt.CreateBrowser(createBrowserReq.Timeout, createBrowserReq.Width, createBrowserReq.Height)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, browserInfo)
}
func (r *Router) deleteBrowserHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.DeleteSession()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) pushImageHandler(c *gin.Context) {
var pushMediaReq PushMediaRequest
if err := c.ShouldBindJSON(&pushMediaReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
imagePath, err := uixt.DownloadFileByUrl(pushMediaReq.ImageUrl)
if path.Ext(imagePath) == "" {
err = os.Rename(imagePath, imagePath+".png")
if err != nil {
RenderError(c, err)
return
}
imagePath = imagePath + ".png"
}
if err != nil {
RenderError(c, err)
return
}
defer func() {
_ = os.Remove(imagePath)
}()
err = driver.PushImage(imagePath)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) clearImageHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.ClearImages()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) videoHandler(c *gin.Context) {
RenderSuccess(c, "")
}

View File

@@ -1,76 +0,0 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func (r *Router) unlockHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Unlock()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) homeHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Home()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) backspaceHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Backspace)
if err != nil {
return
}
count := req.Count
if count == 0 {
count = 20
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Backspace(count)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) keycodeHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_KeyCode)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
// TODO FIXME
err = driver.IDriver.(*uixt.ADBDriver).
PressKeyCode(uixt.KeyCode(req.Keycode), uixt.KMEmpty)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}

View File

@@ -39,47 +39,12 @@ func (r *Router) Init() {
r.Engine.GET("/ping", r.pingHandler)
r.Engine.GET("/", r.pingHandler)
r.Engine.POST("/", r.pingHandler)
r.Engine.GET("/api/v1/devices", r.listDeviceHandler)
r.Engine.POST("/api/v1/browser/create_browser", createBrowserHandler)
apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial")
// tool operations
apiV1PlatformSerial.POST("/tool/invoke", r.invokeToolHandler)
// UI operations
apiV1PlatformSerial.POST("/ui/tap", r.tapHandler)
apiV1PlatformSerial.POST("/ui/right_click", r.rightClickHandler)
apiV1PlatformSerial.POST("/ui/double_tap", r.doubleTapHandler)
apiV1PlatformSerial.POST("/ui/drag", r.dragHandler)
apiV1PlatformSerial.POST("/ui/input", r.inputHandler)
apiV1PlatformSerial.POST("/ui/home", r.homeHandler)
apiV1PlatformSerial.POST("/ui/upload", r.uploadHandler)
apiV1PlatformSerial.POST("/ui/hover", r.hoverHandler)
apiV1PlatformSerial.POST("/ui/scroll", r.scrollHandler)
// Key operations
apiV1PlatformSerial.POST("/key/unlock", r.unlockHandler)
apiV1PlatformSerial.POST("/key/home", r.homeHandler)
apiV1PlatformSerial.POST("/key/backspace", r.backspaceHandler)
apiV1PlatformSerial.POST("/key", r.keycodeHandler)
// APP operations
apiV1PlatformSerial.GET("/app/foreground", r.foregroundAppHandler)
apiV1PlatformSerial.GET("/app/appInfo", r.appInfoHandler)
apiV1PlatformSerial.POST("/app/clear", r.clearAppHandler)
apiV1PlatformSerial.POST("/app/launch", r.launchAppHandler)
apiV1PlatformSerial.POST("/app/terminal", r.terminalAppHandler)
apiV1PlatformSerial.POST("/app/uninstall", r.uninstallAppHandler)
// Device operations
apiV1PlatformSerial.GET("/screenshot", r.screenshotHandler)
apiV1PlatformSerial.DELETE("/close_browser", r.deleteBrowserHandler)
apiV1PlatformSerial.GET("/video", r.videoHandler)
apiV1PlatformSerial.POST("/device/push_image", r.pushImageHandler)
apiV1PlatformSerial.POST("/device/clear_image", r.clearImageHandler)
apiV1PlatformSerial.GET("/adb/source", r.adbSourceHandler)
// uixt operations
apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler)
apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler)

View File

@@ -1,40 +1,7 @@
package server
import (
"github.com/httprunner/httprunner/v5/uixt/option"
)
type uploadRequest struct {
X float64 `json:"x"`
Y float64 `json:"y"`
FileUrl string `json:"file_url"`
FileFormat string `json:"file_format"`
}
type PushMediaRequest struct {
ImageUrl string `json:"imageUrl" binding:"required_without=VideoUrl"`
VideoUrl string `json:"videoUrl" binding:"required_without=ImageUrl"`
}
type HttpResponse struct {
Code int `json:"errorCode"`
Message string `json:"errorMsg"`
Code int `json:"code"`
Message string `json:"message"`
Result interface{} `json:"result,omitempty"`
}
type ScreenRequest struct {
Options *option.ScreenOptions `json:"options,omitempty"`
}
type UploadRequest struct {
X float64 `json:"x"`
Y float64 `json:"y"`
FileUrl string `json:"file_url"`
FileFormat string `json:"file_format"`
}
type CreateBrowserRequest struct {
Timeout int `json:"timeout"`
Width int `json:"width"`
Height int `json:"height"`
}

View File

@@ -1,64 +0,0 @@
package server
import (
"encoding/base64"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt/option"
)
func (r *Router) screenshotHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
raw, err := driver.ScreenShot()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, base64.StdEncoding.EncodeToString(raw.Bytes()))
}
func (r *Router) screenResultHandler(c *gin.Context) {
driver, err := r.GetDriver(c)
if err != nil {
return
}
var screenReq ScreenRequest
if err := c.ShouldBindJSON(&screenReq); err != nil {
RenderErrorValidateRequest(c, err)
return
}
var actionOptions []option.ActionOption
if screenReq.Options != nil {
actionOptions = screenReq.Options.GetScreenShotOptions()
}
screenResult, err := driver.GetScreenResult(actionOptions...)
if err != nil {
log.Err(err).Msg("get screen result failed")
RenderError(c, err)
return
}
RenderSuccess(c, screenResult)
}
func (r *Router) adbSourceHandler(c *gin.Context) {
dExt, err := r.GetDriver(c)
if err != nil {
return
}
source, err := dExt.Source()
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, source)
}

51
server/tool_test.go Normal file
View File

@@ -0,0 +1,51 @@
package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInvokeToolHandler(t *testing.T) {
router := NewRouter()
router.InitMCPHost("../internal/mcp/testdata/test.mcp.json")
tests := []struct {
name string
path string
toolReq ToolRequest
wantStatus int
}{
{
name: "invoke tool",
path: "/api/v1/tool/invoke",
toolReq: ToolRequest{
ServerName: "weather",
ToolName: "get_alerts",
Args: map[string]interface{}{"state": "CA"},
},
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reqBody, _ := json.Marshal(tt.toolReq)
req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
var got HttpResponse
err := json.Unmarshal(w.Body.Bytes(), &got)
assert.NoError(t, err)
})
}
}

View File

@@ -1,210 +0,0 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
// processUnifiedRequest is a helper function to handle common request processing
func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionName) (*option.ActionOptions, error) {
var req option.ActionOptions
// Bind JSON request
if err := c.ShouldBindJSON(&req); err != nil {
RenderErrorValidateRequest(c, err)
return nil, err
}
// Set platform and serial from URL parameters
setRequestContextFromURL(c, &req)
// Validate for HTTP API usage
if err := req.ValidateForHTTPAPI(actionType); err != nil {
RenderErrorValidateRequest(c, err)
return nil, err
}
return &req, nil
}
// setRequestContextFromURL sets platform and serial from URL parameters
func setRequestContextFromURL(c *gin.Context, req *option.ActionOptions) {
if req.Platform == "" {
req.Platform = c.Param("platform")
}
if req.Serial == "" {
req.Serial = c.Param("serial")
}
}
func (r *Router) tapHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Tap)
if err != nil {
return // Error already handled in processUnifiedRequest
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
if req.Duration > 0 {
err = driver.Drag(req.X, req.Y, req.X, req.Y,
option.WithDuration(req.Duration))
} else {
err = driver.TapXY(req.X, req.Y)
}
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) rightClickHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_RightClick)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
SecondaryClick(req.X, req.Y)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) uploadHandler(c *gin.Context) {
var uploadRequest uploadRequest
if err := c.ShouldBindJSON(&uploadRequest); err != nil {
RenderErrorValidateRequest(c, err)
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
UploadFile(uploadRequest.X, uploadRequest.Y,
uploadRequest.FileUrl, uploadRequest.FileFormat)
if err != nil {
c.Abort()
return
}
RenderSuccess(c, true)
}
func (r *Router) hoverHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Hover)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
Hover(req.X, req.Y)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) scrollHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Scroll)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
RenderError(c, err)
return
}
err = driver.IDriver.(*uixt.BrowserDriver).
Scroll(req.Delta)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) doubleTapHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_DoubleTap)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.DoubleTap(req.X, req.Y)
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) dragHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Drag)
if err != nil {
return
}
duration := req.Duration
if duration == 0 {
duration = 1
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Drag(req.FromX, req.FromY, req.ToX, req.ToY,
option.WithDuration(duration),
option.WithPressDuration(req.PressDuration))
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}
func (r *Router) inputHandler(c *gin.Context) {
req, err := r.processUnifiedRequest(c, option.ACTION_Input)
if err != nil {
return
}
driver, err := r.GetDriver(c)
if err != nil {
return
}
err = driver.Input(req.Text, option.WithFrequency(req.Frequency))
if err != nil {
RenderError(c, err)
return
}
RenderSuccess(c, true)
}

View File

@@ -1,114 +0,0 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/stretchr/testify/assert"
)
func TestTapHandler(t *testing.T) {
router := NewRouter()
tests := []struct {
name string
path string
req option.ActionOptions
wantStatus int
wantResp HttpResponse
}{
{
name: "tap abs xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
req: option.ActionOptions{
X: 500.0,
Y: 800.0,
Duration: 0,
},
wantStatus: http.StatusOK,
wantResp: HttpResponse{
Code: 0,
Message: "success",
Result: true,
},
},
{
name: "tap relative xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
req: option.ActionOptions{
X: 0.5,
Y: 0.6,
Duration: 0,
},
wantStatus: http.StatusOK,
wantResp: HttpResponse{
Code: 0,
Message: "success",
Result: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reqBody, _ := json.Marshal(tt.req)
req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
var got HttpResponse
err := json.Unmarshal(w.Body.Bytes(), &got)
assert.NoError(t, err)
assert.Equal(t, tt.wantResp, got)
})
}
}
func TestInvokeToolHandler(t *testing.T) {
router := NewRouter()
router.InitMCPHost("../internal/mcp/testdata/test.mcp.json")
tests := []struct {
name string
path string
toolReq ToolRequest
wantStatus int
}{
{
name: "invoke tool",
path: "/api/v1/tool/invoke",
toolReq: ToolRequest{
ServerName: "weather",
ToolName: "get_alerts",
Args: map[string]interface{}{"state": "CA"},
},
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reqBody, _ := json.Marshal(tt.toolReq)
req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
var got HttpResponse
err := json.Unmarshal(w.Body.Bytes(), &got)
assert.NoError(t, err)
})
}
}

View File

@@ -117,7 +117,6 @@ func (s *MCPServer4XTDriver) registerTools() {
s.registerTool(&ToolClosePopups{})
// PC/Web Tools
s.registerTool(&ToolWebLoginNoneUI{})
s.registerTool(&ToolSecondaryClick{})
s.registerTool(&ToolHoverBySelector{})
s.registerTool(&ToolTapBySelector{})

View File

@@ -3,7 +3,6 @@ package option
import (
"context"
"encoding/json"
"fmt"
"math/rand/v2"
"reflect"
"strings"
@@ -563,188 +562,6 @@ func WithOutputSchema(schema interface{}) ActionOption {
}
}
// HTTP API direct usage methods
// ValidateForHTTPAPI validates the request for HTTP API usage
func (o *ActionOptions) ValidateForHTTPAPI(actionType ActionName) error {
// Basic validation - Platform and Serial are set from URL, so skip here
// They will be validated by setRequestContextFromURL
// Action-specific validation using a more efficient approach
return o.validateActionSpecificFields(actionType)
}
// validateActionSpecificFields performs action-specific field validation
func (o *ActionOptions) validateActionSpecificFields(actionType ActionName) error {
// Define validation rules for each action type using ActionMethod constants
validationRules := map[ActionName]func() error{
ACTION_Tap: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_TapXY: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_TapAbsXY: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_DoubleTap: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_DoubleTapXY: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_RightClick: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_SecondaryClick: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_Hover: func() error {
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
},
ACTION_Drag: func() error {
return o.requireFields("fromX, fromY, toX, toY coordinates",
o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0)
},
ACTION_SwipeCoordinate: func() error {
return o.requireFields("fromX, fromY, toX, toY coordinates",
o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0)
},
ACTION_Swipe: func() error {
return o.requireFields("direction", o.Direction != nil && o.Direction != "")
},
ACTION_SwipeDirection: func() error {
return o.requireFields("direction", o.Direction != nil && o.Direction != "")
},
ACTION_Input: func() error {
return o.requireFields("text", o.Text != "")
},
ACTION_Delete: func() error {
// Count is optional, will use default if not provided
return nil
},
ACTION_Backspace: func() error {
// Count is optional, will use default if not provided
return nil
},
ACTION_KeyCode: func() error {
return o.requireFields("keycode", o.Keycode != 0)
},
ACTION_Scroll: func() error {
return o.requireFields("delta", o.Delta != 0)
},
ACTION_AppInfo: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_AppClear: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_AppLaunch: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_AppTerminate: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_AppUninstall: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_AppInstall: func() error {
return o.requireFields("appUrl", o.AppUrl != "")
},
ACTION_GetForegroundApp: func() error {
return nil
},
ACTION_TapByOCR: func() error {
return o.requireFields("text", o.Text != "")
},
ACTION_SwipeToTapText: func() error {
return o.requireFields("text", o.Text != "")
},
ACTION_TapByCV: func() error {
return o.requireFields("imagePath", o.ImagePath != "")
},
ACTION_SwipeToTapApp: func() error {
return o.requireFields("appName", o.AppName != "")
},
ACTION_SwipeToTapTexts: func() error {
return o.requireFields("texts array", len(o.Texts) > 0)
},
ACTION_TapBySelector: func() error {
return o.requireFields("selector", o.Selector != "")
},
ACTION_HoverBySelector: func() error {
return o.requireFields("selector", o.Selector != "")
},
ACTION_SecondaryClickBySelector: func() error {
return o.requireFields("selector", o.Selector != "")
},
ACTION_WebCloseTab: func() error {
return o.requireFields("tabIndex", o.TabIndex != 0)
},
ACTION_WebLoginNoneUI: func() error {
if o.PackageName == "" || o.PhoneNumber == "" || o.Captcha == "" || o.Password == "" {
return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action")
}
return nil
},
ACTION_SetIme: func() error {
return o.requireFields("ime", o.Ime != "")
},
ACTION_GetSource: func() error {
return o.requireFields("packageName", o.PackageName != "")
},
ACTION_SleepMS: func() error {
return o.requireFields("milliseconds", o.Milliseconds != 0)
},
ACTION_SleepRandom: func() error {
return o.requireFields("params array", len(o.Params) > 0)
},
ACTION_AIAction: func() error {
return o.requireFields("prompt", o.Prompt != "")
},
ACTION_StartToGoal: func() error {
return o.requireFields("prompt", o.Prompt != "")
},
ACTION_Query: func() error {
return o.requireFields("prompt", o.Prompt != "")
},
ACTION_Finished: func() error {
return o.requireFields("content", o.Content != "")
},
ACTION_Upload: func() error {
if o.X == 0 || o.Y == 0 || o.FileUrl == "" {
return fmt.Errorf("x, y coordinates and fileUrl are required for upload action")
}
return nil
},
ACTION_PushMedia: func() error {
if o.ImageUrl == "" && o.VideoUrl == "" {
return fmt.Errorf("either imageUrl or videoUrl is required for push_media action")
}
return nil
},
ACTION_CreateBrowser: func() error {
return o.requireFields("timeout", o.Timeout != 0)
},
}
// Execute validation rule for the action type
if validator, exists := validationRules[actionType]; exists {
return validator()
}
// No specific validation needed for this action type
return nil
}
// requireFields is a helper function to generate consistent error messages
func (o *ActionOptions) requireFields(fieldDesc string, condition bool) error {
if !condition {
return fmt.Errorf("%s is required for this action", fieldDesc)
}
return nil
}
// GetMCPOptions generates MCP tool options for specific action types
func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
// Define field mappings for different action types

View File

@@ -66,7 +66,7 @@ const (
// It helps you to handle an arbitrary element as accept button in accept alert command.
// The selector should be a valid class chain expression, where the search root is the alert element itself.
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] accept`]
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] 'accept'`]
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
)