mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-06 20:32:44 +08:00
refactor: remove unused handlers and related files to streamline the server codebase
This commit is contained in:
@@ -1 +1 @@
|
||||
v5.0.0-beta-2506211542
|
||||
v5.0.0-beta-2506212208
|
||||
|
||||
132
server/app.go
132
server/app.go
@@ -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)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
169
server/device.go
169
server/device.go
@@ -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, "")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
51
server/tool_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
210
server/ui.go
210
server/ui.go
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {'不允许','暂不'}`]"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user