mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-11 07:22:33 +08:00
Merge branch 'master' into session_refactor
This commit is contained in:
@@ -1,35 +1,19 @@
|
||||
//go:build localtest
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hasRequiredEnvVars checks if the required environment variables are set for testing
|
||||
func hasRequiredEnvVars() bool {
|
||||
// Check for OpenAI environment variables
|
||||
if os.Getenv("OPENAI_BASE_URL") != "" && os.Getenv("OPENAI_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
// Check for GPT-4O specific environment variables
|
||||
if os.Getenv("OPENAI_GPT_4O_BASE_URL") != "" && os.Getenv("OPENAI_GPT_4O_API_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestILLMServiceQuery(t *testing.T) {
|
||||
// Skip test if required environment variables are not set
|
||||
if !hasRequiredEnvVars() {
|
||||
t.Skip("Skipping test: required environment variables not set")
|
||||
}
|
||||
|
||||
// Create LLM service
|
||||
service, err := NewLLMService(option.OPENAI_GPT_4O)
|
||||
require.NoError(t, err)
|
||||
@@ -96,11 +80,6 @@ func TestILLMServiceQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestILLMServiceIntegration(t *testing.T) {
|
||||
// Skip test if required environment variables are not set
|
||||
if !hasRequiredEnvVars() {
|
||||
t.Skip("Skipping test: required environment variables not set")
|
||||
}
|
||||
|
||||
// Create LLM service
|
||||
service, err := NewLLMService(option.OPENAI_GPT_4O)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -482,9 +482,10 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
||||
return nil, errors.Wrap(err, "HTTP request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// resp X-Tt-Logid
|
||||
|
||||
logID := resp.Header.Get("X-Tt-Logid")
|
||||
log.Info().Str("step_text", request.StepText).Str("log_id", logID).Str("biz_id", request.BizId).Str("url", w.apiURL).Msg("call wings api")
|
||||
|
||||
// Read response body
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
||||
@@ -706,17 +706,16 @@ func (ad *ADBDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
log.Error().Err(err).Msg("failed to close adb log writer")
|
||||
}
|
||||
pointRes := ConvertPoints(ad.Device.Logcat.logs)
|
||||
|
||||
// 没有解析到打点日志,走兜底逻辑
|
||||
if len(pointRes) == 0 {
|
||||
log.Info().Msg("action log is null, use action file >>>")
|
||||
actionLogDirPath := config.GetConfig().ActionLogDirPath()
|
||||
logFilePathPrefix := fmt.Sprintf("%v/data", actionLogDirPath)
|
||||
files := []string{}
|
||||
ad.Device.RunShellCommand("pull", config.DeviceActionLogFilePath, actionLogDirPath)
|
||||
actionLogRegStr := `.*data_\d+\.txt`
|
||||
ad.Device.PullFolder(config.DeviceActionLogFilePath, actionLogDirPath)
|
||||
err = filepath.Walk(actionLogDirPath, func(path string, info fs.FileInfo, err error) error {
|
||||
// 只是需要日志文件
|
||||
if ok := strings.Contains(path, logFilePathPrefix); ok {
|
||||
if ok, _ := regexp.MatchString(actionLogRegStr, path); ok {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -18,6 +18,7 @@ var (
|
||||
|
||||
// Ensure drivers implement SIMSupport interface
|
||||
_ SIMSupport = (*UIA2Driver)(nil)
|
||||
_ SIMSupport = (*WDADriver)(nil)
|
||||
)
|
||||
|
||||
// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver
|
||||
|
||||
@@ -100,8 +100,7 @@ func (dExt *XTDriver) SwipeToTapTexts(texts []string, opts ...option.ActionOptio
|
||||
}
|
||||
|
||||
log.Info().Strs("texts", texts).Msg("swipe to tap texts")
|
||||
opts = append(opts, option.WithMatchOne(true), option.WithRegex(true), option.WithInterval(1))
|
||||
|
||||
opts = append([]option.ActionOption{option.WithMatchOne(true), option.WithRegex(true), option.WithInterval(1)}, opts...)
|
||||
// Remove identifier for swipe operations to avoid WDA/UIA2 logging
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
actionOptions.Identifier = ""
|
||||
|
||||
@@ -270,8 +270,10 @@ func (s *DriverSession) Request(method string, urlStr string, rawBody []byte, op
|
||||
logger = log.Debug().Bool("success", true)
|
||||
}
|
||||
|
||||
logger = logger.Str("logid", logid).Str("request_method", method).Str("request_url", rawURL).
|
||||
Str("request_body", string(rawBody))
|
||||
logger = logger.Str("logid", logid).Str("request_method", method).Str("request_url", rawURL)
|
||||
if len(rawBody) < 1024 {
|
||||
logger = logger.Str("request_body", string(rawBody))
|
||||
}
|
||||
if !driverResult.RequestTime.IsZero() {
|
||||
logger = logger.Int64("request_time", driverResult.RequestTime.UnixMilli())
|
||||
}
|
||||
|
||||
@@ -284,8 +284,9 @@ func getSimulationDuration(params []float64) (milliseconds int64) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sleepStrict sleeps strict duration with given params
|
||||
// startTime is used to correct sleep duration caused by process time
|
||||
// sleepStrict sleeps for strict duration with optional start time correction
|
||||
// If startTime is zero, acts as normal context-aware sleep
|
||||
// If startTime is provided, corrects sleep duration by subtracting elapsed time
|
||||
// ctx allows for cancellation during sleep
|
||||
func sleepStrict(ctx context.Context, startTime time.Time, strictMilliseconds int64) {
|
||||
var elapsed int64
|
||||
|
||||
116
uixt/image_utils.go
Normal file
116
uixt/image_utils.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// DetectAndRenameImageFile examines the file content to determine its media type
|
||||
// and renames the file with the appropriate extension (.jpg, .png, .mp4, etc.)
|
||||
func DetectAndRenameMediaFile(filePath string) (string, error) {
|
||||
// Open the file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file for type detection: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the first 512 bytes to detect content type
|
||||
buffer := make([]byte, 512)
|
||||
_, err = file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", fmt.Errorf("failed to read file for type detection: %v", err)
|
||||
}
|
||||
|
||||
// Reset file pointer
|
||||
_, err = file.Seek(0, 0)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to reset file pointer: %v", err)
|
||||
}
|
||||
|
||||
// Detect content type
|
||||
contentType := http.DetectContentType(buffer)
|
||||
log.Info().Str("filePath", filePath).Str("contentType", contentType).Msg("Detected content type")
|
||||
|
||||
// Determine file extension based on content type
|
||||
var extension string
|
||||
switch {
|
||||
// Image types
|
||||
case strings.Contains(contentType, "image/jpeg"):
|
||||
extension = ".jpg"
|
||||
case strings.Contains(contentType, "image/png"):
|
||||
extension = ".png"
|
||||
case strings.Contains(contentType, "image/gif"):
|
||||
extension = ".gif"
|
||||
case strings.Contains(contentType, "image/webp"):
|
||||
extension = ".webp"
|
||||
case strings.Contains(contentType, "image/bmp"):
|
||||
extension = ".bmp"
|
||||
case strings.Contains(contentType, "image/tiff"):
|
||||
extension = ".tiff"
|
||||
case strings.Contains(contentType, "image/svg+xml"):
|
||||
extension = ".svg"
|
||||
|
||||
// Video types
|
||||
case strings.Contains(contentType, "video/mp4"):
|
||||
extension = ".mp4"
|
||||
case strings.Contains(contentType, "video/quicktime"):
|
||||
extension = ".mov"
|
||||
case strings.Contains(contentType, "video/x-msvideo"):
|
||||
extension = ".avi"
|
||||
case strings.Contains(contentType, "video/x-ms-wmv"):
|
||||
extension = ".wmv"
|
||||
case strings.Contains(contentType, "video/x-flv"):
|
||||
extension = ".flv"
|
||||
case strings.Contains(contentType, "video/webm"):
|
||||
extension = ".webm"
|
||||
case strings.Contains(contentType, "video/x-matroska"):
|
||||
extension = ".mkv"
|
||||
|
||||
default:
|
||||
// Check for general image or video types
|
||||
if strings.Contains(contentType, "image/") {
|
||||
extension = ".jpg" // Default for unknown image types
|
||||
} else if strings.Contains(contentType, "video/") {
|
||||
extension = ".mp4" // Default for unknown video types
|
||||
} else {
|
||||
// Try to determine from original file extension
|
||||
origExt := strings.ToLower(filepath.Ext(filePath))
|
||||
if origExt == ".mp4" || origExt == ".mov" || origExt == ".avi" ||
|
||||
origExt == ".wmv" || origExt == ".flv" || origExt == ".webm" || origExt == ".mkv" {
|
||||
extension = origExt
|
||||
} else if origExt == ".jpg" || origExt == ".jpeg" || origExt == ".png" ||
|
||||
origExt == ".gif" || origExt == ".webp" || origExt == ".bmp" ||
|
||||
origExt == ".tiff" || origExt == ".svg" {
|
||||
extension = origExt
|
||||
} else {
|
||||
return filePath, fmt.Errorf("not a recognized media type: %s", contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new file path with extension
|
||||
dir := filepath.Dir(filePath)
|
||||
base := filepath.Base(filePath)
|
||||
newFilePath := filepath.Join(dir, base+extension)
|
||||
|
||||
// If the file already has the correct extension, just return it
|
||||
if filePath == newFilePath {
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// Rename the file
|
||||
err = os.Rename(filePath, newFilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to rename file: %v", err)
|
||||
}
|
||||
|
||||
log.Info().Str("oldPath", filePath).Str("newPath", newFilePath).Msg("Renamed image file with proper extension")
|
||||
return newFilePath, nil
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/internal/simulation"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
@@ -692,6 +693,13 @@ func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.Act
|
||||
x, y = toX, toY
|
||||
}
|
||||
|
||||
if x, err = wd.toScale(x); err != nil {
|
||||
return err
|
||||
}
|
||||
if y, err = wd.toScale(y); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var actionMap map[string]interface{}
|
||||
|
||||
switch event.Action {
|
||||
@@ -757,6 +765,201 @@ func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.Act
|
||||
return err
|
||||
}
|
||||
|
||||
// SIMSwipeWithDirection 向指定方向滑动任意距离
|
||||
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||
// fromX, fromY: 起始坐标
|
||||
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
|
||||
func (wd *WDADriver) SIMSwipeWithDirection(direction string, fromX, fromY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
|
||||
absStartX, absStartY, err := convertToAbsolutePoint(wd, fromX, fromY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Str("direction", direction).
|
||||
Float64("startX", absStartX).Float64("startY", absStartY).
|
||||
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeWithDirection")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 转换方向字符串为Direction类型
|
||||
var slideDirection simulation.Direction
|
||||
switch direction {
|
||||
case "up":
|
||||
slideDirection = simulation.Up
|
||||
case "down":
|
||||
slideDirection = simulation.Down
|
||||
case "left":
|
||||
slideDirection = simulation.Left
|
||||
case "right":
|
||||
slideDirection = simulation.Right
|
||||
default:
|
||||
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||
}
|
||||
|
||||
// 使用滑动仿真算法生成触摸事件序列
|
||||
events, err := simulator.GenerateSlideWithRandomDistance(
|
||||
absStartX, absStartY, slideDirection, simMinDistance, simMaxDistance,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate slide events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMSwipeInArea 在指定区域内向指定方向滑动任意距离
|
||||
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||
// simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY: 区域范围(相对坐标)
|
||||
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
|
||||
func (wd *WDADriver) SIMSwipeInArea(direction string, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
|
||||
// 转换区域坐标为绝对坐标
|
||||
absAreaStartX, absAreaStartY, err := convertToAbsolutePoint(wd, simAreaStartX, simAreaStartY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absAreaEndX, absAreaEndY, err := convertToAbsolutePoint(wd, simAreaEndX, simAreaEndY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 确保区域坐标正确(start应该小于等于end)
|
||||
if absAreaStartX > absAreaEndX {
|
||||
absAreaStartX, absAreaEndX = absAreaEndX, absAreaStartX
|
||||
}
|
||||
if absAreaStartY > absAreaEndY {
|
||||
absAreaStartY, absAreaEndY = absAreaEndY, absAreaStartY
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Str("direction", direction).
|
||||
Float64("areaStartX", absAreaStartX).Float64("areaStartY", absAreaStartY).
|
||||
Float64("areaEndX", absAreaEndX).Float64("areaEndY", absAreaEndY).
|
||||
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeInArea")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 转换方向字符串为Direction类型
|
||||
var slideDirection simulation.Direction
|
||||
switch direction {
|
||||
case "up":
|
||||
slideDirection = simulation.Up
|
||||
case "down":
|
||||
slideDirection = simulation.Down
|
||||
case "left":
|
||||
slideDirection = simulation.Left
|
||||
case "right":
|
||||
slideDirection = simulation.Right
|
||||
default:
|
||||
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||
}
|
||||
|
||||
// 使用滑动仿真算法生成区域内滑动的触摸事件序列
|
||||
events, err := simulator.GenerateSlideInArea(
|
||||
absAreaStartX, absAreaStartY, absAreaEndX, absAreaEndY,
|
||||
slideDirection, simMinDistance, simMaxDistance,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate slide in area events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMSwipeFromPointToPoint 指定起始点和结束点进行滑动
|
||||
// fromX, fromY: 起始坐标(相对坐标)
|
||||
// toX, toY: 结束坐标(相对坐标)
|
||||
func (wd *WDADriver) SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
|
||||
// 转换起始点和结束点为绝对坐标
|
||||
absStartX, absStartY, err := convertToAbsolutePoint(wd, fromX, fromY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absEndX, absEndY, err := convertToAbsolutePoint(wd, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Float64("startX", absStartX).Float64("startY", absStartY).
|
||||
Float64("endX", absEndX).Float64("endY", absEndY).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMSwipeFromPointToPoint")
|
||||
|
||||
// 导入滑动仿真库
|
||||
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||
|
||||
// 使用滑动仿真算法生成点对点滑动的触摸事件序列
|
||||
events, err := simulator.GeneratePointToPointSlideEvents(
|
||||
absStartX, absStartY, absEndX, absEndY,
|
||||
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate point to point slide events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
// SIMClickAtPoint 点击相对坐标
|
||||
// x, y: 点击坐标(相对坐标)
|
||||
func (wd *WDADriver) SIMClickAtPoint(x, y float64, opts ...option.ActionOption) error {
|
||||
// 转换为绝对坐标
|
||||
absX, absY, err := convertToAbsolutePoint(wd, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取设备型号和配置参数
|
||||
deviceModel := "iphone"
|
||||
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||
|
||||
log.Info().Float64("x", absX).Float64("y", absY).
|
||||
Str("deviceModel", deviceModel).
|
||||
Int("deviceID", deviceParams.DeviceID).
|
||||
Float64("pressure", deviceParams.Pressure).
|
||||
Float64("size", deviceParams.Size).
|
||||
Msg("WDADriver.SIMClickAtPoint")
|
||||
|
||||
// 导入点击仿真库
|
||||
clickSimulator := simulation.NewClickSimulatorAPI(nil)
|
||||
|
||||
// 使用点击仿真算法生成触摸事件序列
|
||||
events, err := clickSimulator.GenerateClickEvents(
|
||||
absX, absY, deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate click events failed: %v", err)
|
||||
}
|
||||
|
||||
// 执行触摸事件序列
|
||||
return wd.TouchByEvents(events, opts...)
|
||||
}
|
||||
|
||||
func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)]
|
||||
data := map[string]interface{}{
|
||||
@@ -798,6 +1001,69 @@ func (wd *WDADriver) Input(text string, opts ...option.ActionOption) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
// SIMInput 仿真输入函数,模拟人类分批输入行为
|
||||
// 将文本智能分割,英文单词和数字保持完整,中文按1-2个字符分割
|
||||
func (wd *WDADriver) SIMInput(text string, opts ...option.ActionOption) error {
|
||||
log.Info().Str("text", text).Msg("WDADriver.SIMInput")
|
||||
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建输入仿真器(使用默认配置)
|
||||
inputSimulator := simulation.NewInputSimulatorAPI(nil)
|
||||
|
||||
// 生成输入片段(使用智能分割算法,所有参数使用默认值)
|
||||
inputReq := simulation.InputRequest{
|
||||
Text: text,
|
||||
// MinSegmentLen, MaxSegmentLen, MinDelayMs, MaxDelayMs 使用默认值
|
||||
}
|
||||
|
||||
response := inputSimulator.GenerateInputSegments(inputReq)
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to generate input segments: %s", response.Message)
|
||||
}
|
||||
|
||||
log.Info().Int("segments", response.Metrics.TotalSegments).
|
||||
Int("totalDelayMs", response.Metrics.TotalDelayMs).
|
||||
Int("estimatedTimeMs", response.Metrics.EstimatedTimeMs).
|
||||
Msg("Input segments generated")
|
||||
|
||||
// 逐个输入每个片段
|
||||
var segmentErrCnt int
|
||||
for _, segment := range response.Segments {
|
||||
// 使用Input进行输入(内部已包含Session.POST请求)
|
||||
segmentErr := wd.Input(segment.Text, opts...)
|
||||
if segmentErr != nil {
|
||||
segmentErrCnt++
|
||||
log.Info().Err(segmentErr).Int("segmentErrCnt", segmentErrCnt).
|
||||
Msg("segments err")
|
||||
}
|
||||
|
||||
log.Debug().Str("segment", segment.Text).Int("index", segment.Index).
|
||||
Int("charLen", segment.CharLen).Msg("Successfully input segment")
|
||||
|
||||
// 如果有延迟时间,则等待
|
||||
if segment.DelayMs > 0 {
|
||||
time.Sleep(time.Duration(segment.DelayMs) * time.Millisecond)
|
||||
|
||||
log.Debug().Int("delayMs", segment.DelayMs).
|
||||
Msg("Delay between input segments")
|
||||
}
|
||||
}
|
||||
if segmentErrCnt > 0 {
|
||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||
option.MergeOptions(data, opts...)
|
||||
_, err := wd.Session.POST(data, "/wings/interaction/keys")
|
||||
return err
|
||||
}
|
||||
log.Info().Int("totalSegments", response.Metrics.TotalSegments).
|
||||
Int("actualDelayMs", response.Metrics.TotalDelayMs).
|
||||
Msg("SIMInput completed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err error) {
|
||||
log.Info().Int("count", count).Msg("WDADriver.Backspace")
|
||||
if count == 0 {
|
||||
|
||||
@@ -133,6 +133,10 @@ func (s *MCPServer4XTDriver) registerTools() {
|
||||
s.registerTool(&ToolGetScreenSize{})
|
||||
s.registerTool(&ToolGetSource{})
|
||||
|
||||
// Media Album Tools
|
||||
s.registerTool(&ToolPushAlbums{})
|
||||
s.registerTool(&ToolClearAlbums{})
|
||||
|
||||
// Utility Tools
|
||||
s.registerTool(&ToolSleep{})
|
||||
s.registerTool(&ToolSleepMS{})
|
||||
@@ -297,7 +301,7 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
||||
|
||||
// Add tap/swipe offset options
|
||||
if len(tempOptions.TapOffset) == 2 {
|
||||
arguments["tap_offset"] = tempOptions.TapOffset
|
||||
arguments["offset"] = tempOptions.TapOffset
|
||||
}
|
||||
if len(tempOptions.SwipeOffset) == 4 {
|
||||
arguments["swipe_offset"] = tempOptions.SwipeOffset
|
||||
|
||||
@@ -169,27 +169,106 @@ func TestIgnoreNotFoundErrorOption(t *testing.T) {
|
||||
func TestExtractActionOptionsToArguments(t *testing.T) {
|
||||
// Test the extractActionOptionsToArguments helper function
|
||||
actionOptions := []option.ActionOption{
|
||||
// Boolean options
|
||||
option.WithIgnoreNotFoundError(true),
|
||||
option.WithMaxRetryTimes(3),
|
||||
option.WithIndex(2),
|
||||
option.WithRegex(true),
|
||||
option.WithTapRandomRect(false), // false should not be included
|
||||
option.WithDuration(1.5),
|
||||
option.WithAntiRisk(true),
|
||||
option.WithPreMarkOperation(true),
|
||||
option.WithResetHistory(true),
|
||||
option.WithMatchOne(true),
|
||||
|
||||
// Numeric options
|
||||
option.WithMaxRetryTimes(3),
|
||||
option.WithIndex(2),
|
||||
option.WithInterval(1.5),
|
||||
option.WithSteps(10),
|
||||
option.WithTimeout(30),
|
||||
option.WithFrequency(5),
|
||||
option.WithDuration(2.0),
|
||||
option.WithPressDuration(1.5),
|
||||
|
||||
// Offset options (including the fixed offset field)
|
||||
option.WithTapOffset(-300, 0),
|
||||
option.WithSwipeOffset(1, 2, 3, 4),
|
||||
option.WithOffsetRandomRange(-5, 5),
|
||||
|
||||
// Scope options
|
||||
option.WithScope(0.1, 0.2, 0.9, 0.8),
|
||||
option.WithAbsScope(100, 200, 900, 800),
|
||||
|
||||
// Screenshot options
|
||||
option.WithScreenShotOCR(true),
|
||||
option.WithScreenShotUpload(true),
|
||||
option.WithScreenShotLiveType(true),
|
||||
option.WithScreenShotLivePopularity(true),
|
||||
option.WithScreenShotClosePopups(true),
|
||||
option.WithScreenOCRCluster("test_cluster"),
|
||||
option.WithScreenShotFileName("test.png"),
|
||||
option.WithScreenShotUITypes("button", "input"),
|
||||
|
||||
// Direction option
|
||||
option.WithDirection("up"),
|
||||
|
||||
// Identifier
|
||||
option.WithIdentifier("test_id"),
|
||||
}
|
||||
|
||||
arguments := make(map[string]any)
|
||||
extractActionOptionsToArguments(actionOptions, arguments)
|
||||
|
||||
// Verify extracted options
|
||||
// Verify boolean options (only true values should be included)
|
||||
assert.Equal(t, true, arguments["ignore_NotFoundError"], "ignore_NotFoundError should be extracted")
|
||||
assert.Equal(t, 3, arguments["max_retry_times"], "max_retry_times should be extracted")
|
||||
assert.Equal(t, 2, arguments["index"], "index should be extracted")
|
||||
assert.Equal(t, true, arguments["regex"], "regex should be extracted")
|
||||
assert.Equal(t, 1.5, arguments["duration"], "duration should be extracted")
|
||||
assert.Equal(t, true, arguments["anti_risk"], "anti_risk should be extracted")
|
||||
assert.Equal(t, true, arguments["pre_mark_operation"], "pre_mark_operation should be extracted")
|
||||
assert.Equal(t, true, arguments["reset_history"], "reset_history should be extracted")
|
||||
assert.Equal(t, true, arguments["match_one"], "match_one should be extracted")
|
||||
|
||||
// tap_random_rect should not be included since it's false
|
||||
_, exists := arguments["tap_random_rect"]
|
||||
assert.False(t, exists, "tap_random_rect should not be included when false")
|
||||
|
||||
// Verify numeric options
|
||||
assert.Equal(t, 3, arguments["max_retry_times"], "max_retry_times should be extracted")
|
||||
assert.Equal(t, 2, arguments["index"], "index should be extracted")
|
||||
assert.Equal(t, 1.5, arguments["interval"], "interval should be extracted")
|
||||
assert.Equal(t, 10, arguments["steps"], "steps should be extracted")
|
||||
assert.Equal(t, 30, arguments["timeout"], "timeout should be extracted")
|
||||
assert.Equal(t, 5, arguments["frequency"], "frequency should be extracted")
|
||||
assert.Equal(t, 2.0, arguments["duration"], "duration should be extracted")
|
||||
assert.Equal(t, 1.5, arguments["press_duration"], "press_duration should be extracted")
|
||||
|
||||
// Verify offset options (including the critical 'offset' field that was fixed)
|
||||
assert.Equal(t, []int{-300, 0}, arguments["offset"], "offset should be extracted (not tap_offset)")
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, arguments["swipe_offset"], "swipe_offset should be extracted")
|
||||
assert.Equal(t, []int{-5, 5}, arguments["offset_random_range"], "offset_random_range should be extracted")
|
||||
|
||||
// Verify scope options (these are custom types, not raw slices)
|
||||
assert.Equal(t, option.Scope([]float64{0.1, 0.2, 0.9, 0.8}), arguments["scope"], "scope should be extracted")
|
||||
assert.Equal(t, option.AbsScope([]int{100, 200, 900, 800}), arguments["abs_scope"], "abs_scope should be extracted")
|
||||
|
||||
// Verify screenshot options
|
||||
assert.Equal(t, true, arguments["screenshot_with_ocr"], "screenshot_with_ocr should be extracted")
|
||||
assert.Equal(t, true, arguments["screenshot_with_upload"], "screenshot_with_upload should be extracted")
|
||||
assert.Equal(t, true, arguments["screenshot_with_live_type"], "screenshot_with_live_type should be extracted")
|
||||
assert.Equal(t, true, arguments["screenshot_with_live_popularity"], "screenshot_with_live_popularity should be extracted")
|
||||
assert.Equal(t, true, arguments["screenshot_with_close_popups"], "screenshot_with_close_popups should be extracted")
|
||||
assert.Equal(t, "test_cluster", arguments["screenshot_with_ocr_cluster"], "screenshot_with_ocr_cluster should be extracted")
|
||||
assert.Equal(t, "test.png", arguments["screenshot_file_name"], "screenshot_file_name should be extracted")
|
||||
assert.Equal(t, []string{"button", "input"}, arguments["screenshot_with_ui_types"], "screenshot_with_ui_types should be extracted")
|
||||
|
||||
// Verify identifier and direction (only fields that exist)
|
||||
assert.Equal(t, "test_id", arguments["identifier"], "identifier should be extracted")
|
||||
assert.Equal(t, "up", arguments["direction"], "direction should be extracted")
|
||||
|
||||
// Verify the critical fix: ensure "offset" is used instead of "tap_offset"
|
||||
_, hasTapOffset := arguments["tap_offset"]
|
||||
assert.False(t, hasTapOffset, "Should NOT contain 'tap_offset' field")
|
||||
_, hasOffset := arguments["offset"]
|
||||
assert.True(t, hasOffset, "Should contain 'offset' field")
|
||||
|
||||
t.Logf("Extracted %d arguments from ActionOptions", len(arguments))
|
||||
}
|
||||
|
||||
// TestToolListAvailableDevices tests the ToolListAvailableDevices implementation
|
||||
|
||||
@@ -3,6 +3,9 @@ package uixt
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/danielpaulus/go-ios/ios"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -216,3 +219,198 @@ func (t *ToolScreenRecord) Implement() server.ToolHandlerFunc {
|
||||
func (t *ToolScreenRecord) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
return BuildMCPCallToolRequest(t.Name(), map[string]any{}, action), nil
|
||||
}
|
||||
|
||||
// ToolPushAlbums implements the push_albums tool call.
|
||||
type ToolPushAlbums struct {
|
||||
// Return data fields - these define the structure of data returned by this tool
|
||||
FilePath string `json:"filePath" desc:"Path of the file that was pushed"`
|
||||
FileUrl string `json:"fileUrl,omitempty" desc:"URL of the file that was downloaded and pushed (if applicable)"`
|
||||
FileType string `json:"fileType" desc:"Type of the file that was pushed (image or video)"`
|
||||
Cleared bool `json:"cleared,omitempty" desc:"Whether albums were cleared before pushing (if applicable)"`
|
||||
}
|
||||
|
||||
func (t *ToolPushAlbums) Name() option.ActionName {
|
||||
return option.ACTION_PushAlbums
|
||||
}
|
||||
|
||||
func (t *ToolPushAlbums) Description() string {
|
||||
return "Push a media file (image or video) to the device's gallery. For Android, this will push the file to the DCIM/Camera directory. For iOS, this will add the file to the photo album."
|
||||
}
|
||||
|
||||
func (t *ToolPushAlbums) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithString("platform", mcp.Enum("android", "ios"), mcp.Description("The platform type of device to push media to")),
|
||||
mcp.WithString("serial", mcp.Description("The device serial number or UDID")),
|
||||
mcp.WithString("filePath", mcp.Description("Path to the local media file to push to the device")),
|
||||
mcp.WithString("fileUrl", mcp.Description("URL of the media file to download and push to the device")),
|
||||
mcp.WithBoolean("cleanup", mcp.Description("Whether to delete the downloaded file after pushing it to the device")),
|
||||
mcp.WithBoolean("clearBefore", mcp.Description("Whether to clear albums before pushing (if applicable)")),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ToolPushAlbums) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{}
|
||||
|
||||
// Handle string param as fileUrl
|
||||
if fileUrl, ok := action.Params.(string); ok && fileUrl != "" {
|
||||
arguments["fileUrl"] = fileUrl
|
||||
}
|
||||
|
||||
// Handle map params with fileUrl or filePath
|
||||
if params, ok := action.Params.(map[string]interface{}); ok {
|
||||
if fileUrl, ok := params["fileUrl"].(string); ok && fileUrl != "" {
|
||||
arguments["fileUrl"] = fileUrl
|
||||
}
|
||||
if filePath, ok := params["filePath"].(string); ok && filePath != "" {
|
||||
arguments["filePath"] = filePath
|
||||
}
|
||||
if cleanup, ok := params["cleanup"].(bool); ok {
|
||||
arguments["cleanup"] = cleanup
|
||||
}
|
||||
if clearBefore, ok := params["clearBefore"].(bool); ok {
|
||||
arguments["clearBefore"] = clearBefore
|
||||
}
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
func (t *ToolPushAlbums) Implement() server.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
driverExt, err := setupXTDriver(ctx, request.GetArguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get file path or URL
|
||||
filePath, hasPath := request.GetArguments()["filePath"].(string)
|
||||
fileUrl, hasUrl := request.GetArguments()["fileUrl"].(string)
|
||||
cleanup, _ := request.GetArguments()["cleanup"].(bool)
|
||||
clearBefore, _ := request.GetArguments()["clearBefore"].(bool)
|
||||
|
||||
// Check if we have either path or URL
|
||||
if (!hasPath || filePath == "") && (!hasUrl || fileUrl == "") {
|
||||
return nil, fmt.Errorf("either filePath or fileUrl is required")
|
||||
}
|
||||
|
||||
// If we have a URL, download it
|
||||
downloadedFile := false
|
||||
fileType := "image" // Default file type
|
||||
if hasUrl && fileUrl != "" {
|
||||
log.Info().Str("fileUrl", fileUrl).Msg("Downloading media file from URL")
|
||||
downloadedPath, err := DownloadFileByUrl(fileUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download media file from URL: %v", err)
|
||||
}
|
||||
|
||||
// Detect file type and rename with proper extension
|
||||
renamedPath, err := DetectAndRenameMediaFile(downloadedPath)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("path", downloadedPath).Msg("Failed to detect file type or rename file, using original file")
|
||||
filePath = downloadedPath
|
||||
} else {
|
||||
filePath = renamedPath
|
||||
// Determine if it's a video based on extension
|
||||
ext := strings.ToLower(filepath.Ext(renamedPath))
|
||||
if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".wmv" || ext == ".flv" || ext == ".webm" || ext == ".mkv" {
|
||||
fileType = "video"
|
||||
}
|
||||
}
|
||||
downloadedFile = true
|
||||
}
|
||||
|
||||
// Clear albums before pushing if requested
|
||||
cleared := false
|
||||
if clearBefore {
|
||||
log.Info().Msg("Clearing albums before pushing new media file")
|
||||
err := driverExt.IDriver.ClearImages()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to clear albums before pushing, continuing anyway")
|
||||
} else {
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
|
||||
// Push the file to the device
|
||||
err = driverExt.IDriver.PushImage(filePath)
|
||||
if err != nil {
|
||||
// If we downloaded the file and failed to push it, clean up
|
||||
if downloadedFile && cleanup {
|
||||
_ = os.Remove(filePath)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clean up downloaded file if requested
|
||||
if downloadedFile && cleanup {
|
||||
log.Info().Str("filePath", filePath).Msg("Cleaning up downloaded media file")
|
||||
_ = os.Remove(filePath)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Successfully pushed %s to device", fileType)
|
||||
returnData := ToolPushAlbums{
|
||||
FilePath: filePath,
|
||||
FileType: fileType,
|
||||
Cleared: cleared,
|
||||
}
|
||||
|
||||
// Include URL in response if it was used
|
||||
if hasUrl && fileUrl != "" {
|
||||
returnData.FileUrl = fileUrl
|
||||
message = fmt.Sprintf("Successfully downloaded and pushed %s from %s to device", fileType, fileUrl)
|
||||
}
|
||||
|
||||
// Add cleared info to message if applicable
|
||||
if cleared {
|
||||
message = fmt.Sprintf("%s (albums cleared before pushing)", message)
|
||||
}
|
||||
|
||||
return NewMCPSuccessResponse(message, &returnData), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Old ToolPushImage implementation has been removed as part of the refactoring to ToolPushAlbums
|
||||
|
||||
// ToolClearAlbums implements the clear_albums tool call.
|
||||
type ToolClearAlbums struct {
|
||||
// Return data fields - these define the structure of data returned by this tool
|
||||
Cleared bool `json:"cleared" desc:"Whether albums were cleared successfully"`
|
||||
}
|
||||
|
||||
func (t *ToolClearAlbums) Name() option.ActionName {
|
||||
return option.ACTION_ClearAlbums
|
||||
}
|
||||
|
||||
func (t *ToolClearAlbums) Description() string {
|
||||
return "Clear media files (images and videos) from the device's gallery. For Android, this will clear media from the DCIM/Camera directory. For iOS, this will clear media from the device's photo album."
|
||||
}
|
||||
|
||||
func (t *ToolClearAlbums) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithString("platform", mcp.Enum("android", "ios"), mcp.Description("The platform type of device to clear media from")),
|
||||
mcp.WithString("serial", mcp.Description("The device serial number or UDID")),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ToolClearAlbums) Implement() server.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
driverExt, err := setupXTDriver(ctx, request.GetArguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = driverExt.IDriver.ClearImages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := "Successfully cleared media files from device"
|
||||
returnData := ToolClearAlbums{Cleared: true}
|
||||
|
||||
return NewMCPSuccessResponse(message, &returnData), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ToolClearAlbums) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
return BuildMCPCallToolRequest(t.Name(), map[string]any{}, action), nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package uixt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -15,7 +13,29 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolSleep implements the sleep tool call.
|
||||
// extractStartTimeMs extracts start_time_ms from MCP request arguments
|
||||
// Returns time.Time (zero if not provided) and any conversion error
|
||||
func extractStartTimeMs(request mcp.CallToolRequest) (time.Time, error) {
|
||||
startTimeMs, ok := request.GetArguments()["start_time_ms"]
|
||||
if !ok || startTimeMs == nil {
|
||||
return time.Time{}, nil // Return zero time for normal sleep
|
||||
}
|
||||
|
||||
var ms int64
|
||||
switch v := startTimeMs.(type) {
|
||||
case float64:
|
||||
ms = int64(v)
|
||||
case int64:
|
||||
ms = v
|
||||
case int:
|
||||
ms = int64(v)
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("invalid start_time_ms type: %T", v)
|
||||
}
|
||||
|
||||
return time.UnixMilli(ms), nil
|
||||
}
|
||||
|
||||
type ToolSleep struct {
|
||||
// Return data fields - these define the structure of data returned by this tool
|
||||
Seconds float64 `json:"seconds" desc:"Duration in seconds that was slept"`
|
||||
@@ -33,6 +53,7 @@ func (t *ToolSleep) Description() string {
|
||||
func (t *ToolSleep) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("seconds", mcp.Description("Number of seconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,38 +68,21 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
// Sleep action logic
|
||||
log.Info().Interface("seconds", seconds).Msg("sleeping")
|
||||
|
||||
var duration time.Duration
|
||||
var actualSeconds float64
|
||||
switch v := seconds.(type) {
|
||||
case float64:
|
||||
actualSeconds = v
|
||||
duration = time.Duration(v*1000) * time.Millisecond
|
||||
case int:
|
||||
actualSeconds = float64(v)
|
||||
duration = time.Duration(v) * time.Second
|
||||
case int64:
|
||||
actualSeconds = float64(v)
|
||||
duration = time.Duration(v) * time.Second
|
||||
case string:
|
||||
s, err := builtin.ConvertToFloat64(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sleep duration: %v", v)
|
||||
}
|
||||
actualSeconds = s
|
||||
duration = time.Duration(s*1000) * time.Millisecond
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
// Use Interface2Float64 for unified type conversion
|
||||
actualSeconds, err := builtin.Interface2Float64(seconds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sleep duration: %v", seconds)
|
||||
}
|
||||
duration := time.Duration(actualSeconds) * time.Second
|
||||
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
}
|
||||
milliseconds := int64(actualSeconds * 1000)
|
||||
sleepStrict(ctx, startTime, milliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %v seconds", actualSeconds)
|
||||
returnData := ToolSleep{
|
||||
@@ -91,9 +95,24 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleep) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{
|
||||
"seconds": action.Params,
|
||||
arguments := map[string]any{}
|
||||
|
||||
var seconds float64
|
||||
if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both seconds and startTime
|
||||
seconds = sleepConfig.Seconds
|
||||
arguments["seconds"] = seconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
// Use builtin.Interface2Float64 for unified parameter handling
|
||||
var err error
|
||||
seconds, err = builtin.Interface2Float64(action.Params)
|
||||
if err != nil {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep params: %v", action.Params)
|
||||
}
|
||||
arguments["seconds"] = seconds
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
@@ -115,6 +134,7 @@ func (t *ToolSleepMS) Description() string {
|
||||
func (t *ToolSleepMS) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("milliseconds", mcp.Description("Number of milliseconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,38 +149,21 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
// Sleep MS action logic
|
||||
log.Info().Interface("milliseconds", milliseconds).Msg("sleeping in milliseconds")
|
||||
|
||||
var duration time.Duration
|
||||
var actualMilliseconds int64
|
||||
switch v := milliseconds.(type) {
|
||||
case float64:
|
||||
actualMilliseconds = int64(v)
|
||||
duration = time.Duration(v) * time.Millisecond
|
||||
case int:
|
||||
actualMilliseconds = int64(v)
|
||||
duration = time.Duration(v) * time.Millisecond
|
||||
case int64:
|
||||
actualMilliseconds = v
|
||||
duration = time.Duration(v) * time.Millisecond
|
||||
case string:
|
||||
ms, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sleep duration: %v", v)
|
||||
}
|
||||
actualMilliseconds = ms
|
||||
duration = time.Duration(ms) * time.Millisecond
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
// Use Interface2Float64 for unified type conversion, then convert to int64
|
||||
floatVal, err := builtin.Interface2Float64(milliseconds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sleep duration: %v", milliseconds)
|
||||
}
|
||||
actualMilliseconds := int64(floatVal)
|
||||
duration := time.Duration(actualMilliseconds) * time.Millisecond
|
||||
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
}
|
||||
sleepStrict(ctx, startTime, actualMilliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %d milliseconds", actualMilliseconds)
|
||||
returnData := ToolSleepMS{
|
||||
@@ -173,17 +176,24 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleepMS) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{}
|
||||
|
||||
var milliseconds int64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ = param.Int64()
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
milliseconds = param
|
||||
if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both milliseconds and startTime
|
||||
milliseconds = sleepConfig.Milliseconds
|
||||
arguments["milliseconds"] = milliseconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
|
||||
}
|
||||
arguments := map[string]any{
|
||||
"milliseconds": milliseconds,
|
||||
// Use builtin.Interface2Float64 for unified parameter handling, then convert to int64
|
||||
floatVal, err := builtin.Interface2Float64(action.Params)
|
||||
if err != nil {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
|
||||
}
|
||||
milliseconds = int64(floatVal)
|
||||
arguments["milliseconds"] = milliseconds
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
|
||||
285
uixt/mcp_tools_utility_test.go
Normal file
285
uixt/mcp_tools_utility_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestToolSleep_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleep{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: json.Number("3.5"),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(3.5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "float64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: float64(5.2),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(5.2)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: int64(5),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Seconds: 2.5,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"seconds": 2.5,
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "json.Number with integer value",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: json.Number("10"),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(10)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "json.Number with decimal value",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: json.Number("1.25"),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(1.25)},
|
||||
shouldError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolSleepMS_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleepMS{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: json.Number("1500"),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(1500)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: int64(2000),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(2000)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "float64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: float64(2500.7),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(2500)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Milliseconds: 3000,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"milliseconds": int64(3000),
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "json.Number with decimal value",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: json.Number("1234.56"),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(1234)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSleepStrictTiming(t *testing.T) {
|
||||
// Test that strict sleep properly adjusts for elapsed time
|
||||
startTime := time.Now()
|
||||
|
||||
// Simulate some processing time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test sleepStrict with the start time
|
||||
testStart := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // 200ms total duration
|
||||
actualElapsed := time.Since(testStart)
|
||||
|
||||
// Should sleep approximately 150ms (200ms - 50ms already elapsed)
|
||||
// Allow some tolerance for timing variations
|
||||
expectedSleep := 150 * time.Millisecond
|
||||
assert.Greater(t, actualElapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, actualElapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepCancellation(t *testing.T) {
|
||||
// Test that sleep respects context cancellation
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Cancel after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 500) // Try to sleep 500ms
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled after ~50ms, not sleep full 500ms
|
||||
assert.Less(t, elapsed, 200*time.Millisecond, "Sleep was not properly cancelled")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithZeroTime(t *testing.T) {
|
||||
// Test sleepStrict behaves like normal sleep when startTime is zero
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 100) // 100ms, no start time
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should sleep full duration
|
||||
expectedSleep := 100 * time.Millisecond
|
||||
assert.Greater(t, elapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, elapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithPastStartTime(t *testing.T) {
|
||||
// Test sleepStrict skips sleep when elapsed time exceeds duration
|
||||
startTime := time.Now().Add(-300 * time.Millisecond) // 300ms ago
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // Want 200ms total, but 300ms already elapsed
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should skip sleep entirely
|
||||
assert.Less(t, elapsed, 50*time.Millisecond, "Should have skipped sleep")
|
||||
}
|
||||
|
||||
func TestJsonNumberHandling(t *testing.T) {
|
||||
// Test that json.Number is correctly handled in different scenarios
|
||||
|
||||
// Test float json.Number
|
||||
floatNumber := json.Number("3.14")
|
||||
floatVal, err := floatNumber.Float64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3.14, floatVal)
|
||||
|
||||
// Test int json.Number
|
||||
intNumber := json.Number("1500")
|
||||
intVal, err := intNumber.Int64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1500), intVal)
|
||||
|
||||
// Test invalid json.Number
|
||||
invalidNumber := json.Number("invalid")
|
||||
_, err = invalidNumber.Float64()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -98,6 +98,10 @@ const (
|
||||
ACTION_ListAvailableDevices ActionName = "list_available_devices"
|
||||
ACTION_SelectDevice ActionName = "select_device"
|
||||
|
||||
// album actions (images and videos)
|
||||
ACTION_PushAlbums ActionName = "push_albums"
|
||||
ACTION_ClearAlbums ActionName = "clear_albums"
|
||||
|
||||
// custom actions
|
||||
ACTION_SwipeToTapApp ActionName = "swipe_to_tap_app" // swipe left & right to find app and tap
|
||||
ACTION_SwipeToTapText ActionName = "swipe_to_tap_text" // swipe up & down to find text and tap
|
||||
|
||||
@@ -66,22 +66,21 @@ func ParseTouchEvents(data string) ([]types.TouchEvent, error) {
|
||||
if event.Action, err = strconv.Atoi(parts[12]); err != nil {
|
||||
return nil, fmt.Errorf("invalid action: %v", err)
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func TestAndroidTouchByEvents(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
func TestIOSTouchByEvents(t *testing.T) {
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -138,60 +137,6 @@ func TestAndroidTouchByEvents(t *testing.T) {
|
||||
t.Logf("Successfully executed touch events: %d events processed", len(events))
|
||||
}
|
||||
|
||||
func TestIOSTouchByEvents(t *testing.T) {
|
||||
driver := setupWDADriverExt(t)
|
||||
|
||||
// Example touch event data as provided
|
||||
touchEventData := `1752649131556,401.20703,1191.3164,2,1.0,0.03529412,457.20703,1359.3164,111586196,111586196,1,0,0
|
||||
1752649131595,402.913,1185.0792,2,1.0,0.039215688,458.913,1353.0792,111586196,111586236,1,0,2
|
||||
1752649131612,410.60825,1164.3806,2,1.0,0.03529412,466.60825,1332.3806,111586196,111586250,1,0,2
|
||||
1752649131629,437.7335,1093.1417,2,1.0,0.039215688,493.7335,1261.1417,111586196,111586270,1,0,2
|
||||
1752649131646,463.5786,1018.01746,2,1.0,0.039215688,519.5786,1186.0175,111586196,111586287,1,0,2
|
||||
1752649131662,487.56482,948.9773,2,1.0,0.03529412,543.5648,1116.9773,111586196,111586304,1,0,2
|
||||
1752649131679,511.81476,881.6183,2,1.0,0.039215688,567.81476,1049.6183,111586196,111586320,1,0,2
|
||||
1752649131696,543.4369,811.4982,2,1.0,0.03529412,599.4369,979.4982,111586196,111586337,1,0,2
|
||||
1752649131713,577.1632,747.4512,2,1.0,0.039215688,633.1632,915.4512,111586196,111586354,1,0,2
|
||||
1752649131729,610.1538,691.72034,2,1.0,0.03529412,666.1538,859.72034,111586196,111586370,1,0,2
|
||||
1752649131746,639.1683,642.6914,2,1.0,0.03529412,695.1683,810.6914,111586196,111586387,1,0,2
|
||||
1752649131763,658.9832,605.90857,2,1.0,0.03529412,714.9832,773.90857,111586196,111586404,1,0,2
|
||||
1752649131779,672.21954,581.1634,2,1.0,0.03529412,728.21954,749.1634,111586196,111586420,1,0,2
|
||||
1752649131796,680.7687,566.1778,2,1.0,0.03529412,736.7687,734.1778,111586196,111586434,1,0,2
|
||||
1752649131814,688.0894,554.2295,2,1.0,0.03529412,744.0894,722.2295,111586196,111586450,1,0,2
|
||||
1752649131830,694.542,544.7783,2,1.0,0.03529412,750.542,712.7783,111586196,111586466,1,0,2
|
||||
1752649131847,700.60645,537.2637,2,1.0,0.039215688,756.60645,705.2637,111586196,111586483,1,0,2
|
||||
1752649131863,705.08887,531.1406,2,1.0,0.039215688,761.08887,699.1406,111586196,111586500,1,0,2
|
||||
1752649131880,708.1211,527.8008,2,1.0,0.039215688,764.1211,695.8008,111586196,111586517,1,0,2
|
||||
1752649131897,709.43945,524.46094,2,1.0,0.039215688,765.43945,692.46094,111586196,111586533,1,0,2
|
||||
1752649131902,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586537,1,33554432,2
|
||||
1752649131907,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586546,1,0,1`
|
||||
|
||||
// Parse touch events
|
||||
events, err := ParseTouchEvents(touchEventData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTouchEvents failed: %v", err)
|
||||
}
|
||||
|
||||
// Check first event
|
||||
firstEvent := events[0]
|
||||
if firstEvent.Action != 0 { // ACTION_DOWN
|
||||
t.Errorf("Expected first event action to be 0 (ACTION_DOWN), got %d", firstEvent.Action)
|
||||
}
|
||||
|
||||
// Check last event
|
||||
lastEvent := events[len(events)-1]
|
||||
if lastEvent.Action != 1 { // ACTION_UP
|
||||
t.Errorf("Expected last event action to be 1 (ACTION_UP), got %d", lastEvent.Action)
|
||||
}
|
||||
|
||||
// Use TouchByEvents with parsed events
|
||||
err = driver.IDriver.(*WDADriver).TouchByEvents(events)
|
||||
if err != nil {
|
||||
t.Fatalf("TouchByEvents failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Successfully executed touch events: %d events processed", len(events))
|
||||
}
|
||||
|
||||
func TestTouchEventParsing(t *testing.T) {
|
||||
// Test single touch event parsing
|
||||
singleEventData := "1752646457403,456.78418,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0"
|
||||
@@ -281,14 +226,14 @@ func TestTouchEventSequenceValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeWithDirection(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -308,7 +253,7 @@ func TestSwipeWithDirection(t *testing.T) {
|
||||
direction: "up",
|
||||
startX: 0.5,
|
||||
startY: 0.5,
|
||||
minDistance: 100.0,
|
||||
minDistance: 500.0,
|
||||
maxDistance: 500.0,
|
||||
},
|
||||
}
|
||||
@@ -332,50 +277,15 @@ func TestSwipeWithDirection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwipeWithDirectionInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer driver.TearDown()
|
||||
|
||||
// Test invalid direction
|
||||
err = driver.SIMSwipeWithDirection("invalid", 500.0, 500.0, 100.0, 200.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid direction, but got none")
|
||||
}
|
||||
|
||||
// Test invalid distance range (max < min)
|
||||
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 200.0, 100.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid distance range, but got none")
|
||||
}
|
||||
|
||||
// Test zero distance
|
||||
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 0.0, 0.0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for zero distance, but got none")
|
||||
}
|
||||
|
||||
t.Log("Invalid input validation tests passed")
|
||||
}
|
||||
|
||||
func TestSwipeInArea(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -428,14 +338,14 @@ func TestSwipeInArea(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeFromPointToPoint(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -477,14 +387,14 @@ func TestSwipeFromPointToPoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwipeFromPointToPointInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -506,14 +416,14 @@ func TestSwipeFromPointToPointInvalidInputs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClickAtPoint(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -546,14 +456,14 @@ func TestClickAtPoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClickAtPointInvalidInputs(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -580,14 +490,14 @@ func TestClickAtPointInvalidInputs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSIMInput(t *testing.T) {
|
||||
device, err := NewAndroidDevice(
|
||||
option.WithSerialNumber(""),
|
||||
device, err := NewIOSDevice(
|
||||
option.WithUDID(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err := NewUIA2Driver(device)
|
||||
driver, err := NewWDADriver(device)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user