mirror of
https://github.com/httprunner/httprunner.git
synced 2026-08-28 19:47:14 +08:00
fix: MCP server ignore_NotFoundError option not working
- Fixed TapByOCR and TapByCV tools to properly handle ignore_NotFoundError option - Added option parameters to all MCP tool request structures - Fixed ConvertActionToCallToolRequest methods to extract action options - Added extractActionOptionsToArguments helper function for consistent option handling - Extended fix to all MCP tools: SwipeToTapApp, SwipeToTapText, SwipeToTapTexts, TapXY, TapAbsXY - Added comprehensive tests for option parameter handling - Updated test expectations to match actual registered tools This ensures that when ignore_NotFoundError is set to true, OCR/CV operations will return nil instead of throwing errors when target elements are not found, allowing tests to continue execution as expected.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2505262125
|
v5.0.0-beta-2505262202
|
||||||
|
|||||||
+206
-14
@@ -313,12 +313,27 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if tapReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if tapReq.Duration > 0 {
|
||||||
|
opts = append(opts, option.WithDuration(tapReq.Duration))
|
||||||
|
}
|
||||||
|
if tapReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(tapReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default options
|
||||||
|
opts = append(opts, option.WithPreMarkOperation(true))
|
||||||
|
|
||||||
// Tap action logic
|
// Tap action logic
|
||||||
log.Info().Float64("x", tapReq.X).Float64("y", tapReq.Y).Msg("tapping at coordinates")
|
log.Info().Float64("x", tapReq.X).Float64("y", tapReq.Y).Msg("tapping at coordinates")
|
||||||
opts := []option.ActionOption{
|
|
||||||
option.WithDuration(tapReq.Duration),
|
|
||||||
option.WithPreMarkOperation(true),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = driverExt.TapXY(tapReq.X, tapReq.Y, opts...)
|
err = driverExt.TapXY(tapReq.X, tapReq.Y, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -340,6 +355,10 @@ func (t *ToolTapXY) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal
|
|||||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||||
arguments["duration"] = duration
|
arguments["duration"] = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params)
|
||||||
@@ -372,12 +391,24 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tap absolute XY action logic
|
// Build action options from request structure
|
||||||
log.Info().Float64("x", tapAbsReq.X).Float64("y", tapAbsReq.Y).Msg("tapping at absolute coordinates")
|
var opts []option.ActionOption
|
||||||
opts := []option.ActionOption{}
|
|
||||||
|
// Add boolean options
|
||||||
|
if tapAbsReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
if tapAbsReq.Duration > 0 {
|
if tapAbsReq.Duration > 0 {
|
||||||
opts = append(opts, option.WithDuration(tapAbsReq.Duration))
|
opts = append(opts, option.WithDuration(tapAbsReq.Duration))
|
||||||
}
|
}
|
||||||
|
if tapAbsReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(tapAbsReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap absolute XY action logic
|
||||||
|
log.Info().Float64("x", tapAbsReq.X).Float64("y", tapAbsReq.Y).Msg("tapping at absolute coordinates")
|
||||||
|
|
||||||
err = driverExt.TapAbsXY(tapAbsReq.X, tapAbsReq.Y, opts...)
|
err = driverExt.TapAbsXY(tapAbsReq.X, tapAbsReq.Y, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -399,12 +430,16 @@ func (t *ToolTapAbsXY) ConvertActionToCallToolRequest(action MobileAction) (mcp.
|
|||||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||||
arguments["duration"] = duration
|
arguments["duration"] = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolTapByOCR implements the tap_by_ocr tool call.
|
// ToolTapByOCR implements the tap_ocr tool call.
|
||||||
type ToolTapByOCR struct{}
|
type ToolTapByOCR struct{}
|
||||||
|
|
||||||
func (t *ToolTapByOCR) Name() option.ActionMethod {
|
func (t *ToolTapByOCR) Name() option.ActionMethod {
|
||||||
@@ -431,9 +466,31 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if ocrReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
if ocrReq.Regex {
|
||||||
|
opts = append(opts, option.WithRegex(true))
|
||||||
|
}
|
||||||
|
if ocrReq.TapRandomRect {
|
||||||
|
opts = append(opts, option.WithTapRandomRect(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if ocrReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(ocrReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
if ocrReq.Index > 0 {
|
||||||
|
opts = append(opts, option.WithIndex(ocrReq.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// Tap by OCR action logic
|
// Tap by OCR action logic
|
||||||
log.Info().Str("text", ocrReq.Text).Msg("tapping by OCR")
|
log.Info().Str("text", ocrReq.Text).Msg("tapping by OCR")
|
||||||
err = driverExt.TapByOCR(ocrReq.Text)
|
err = driverExt.TapByOCR(ocrReq.Text, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Tap by OCR failed: %s", err.Error())), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Tap by OCR failed: %s", err.Error())), nil
|
||||||
}
|
}
|
||||||
@@ -447,12 +504,16 @@ func (t *ToolTapByOCR) ConvertActionToCallToolRequest(action MobileAction) (mcp.
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"text": text,
|
"text": text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolTapByCV implements the tap_by_cv tool call.
|
// ToolTapByCV implements the tap_cv tool call.
|
||||||
type ToolTapByCV struct{}
|
type ToolTapByCV struct{}
|
||||||
|
|
||||||
func (t *ToolTapByCV) Name() option.ActionMethod {
|
func (t *ToolTapByCV) Name() option.ActionMethod {
|
||||||
@@ -479,13 +540,32 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if cvReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
if cvReq.TapRandomRect {
|
||||||
|
opts = append(opts, option.WithTapRandomRect(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if cvReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(cvReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
if cvReq.Index > 0 {
|
||||||
|
opts = append(opts, option.WithIndex(cvReq.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// Tap by CV action logic
|
// Tap by CV action logic
|
||||||
log.Info().Str("imagePath", cvReq.ImagePath).Msg("tapping by CV")
|
log.Info().Str("imagePath", cvReq.ImagePath).Msg("tapping by CV")
|
||||||
|
|
||||||
// For TapByCV, we need to check if there are UI types in the options
|
// For TapByCV, we need to check if there are UI types in the options
|
||||||
// In the original DoAction, it requires ScreenShotWithUITypes to be set
|
// In the original DoAction, it requires ScreenShotWithUITypes to be set
|
||||||
// We'll add a basic implementation that triggers CV recognition
|
// We'll add a basic implementation that triggers CV recognition
|
||||||
err = driverExt.TapByCV()
|
err = driverExt.TapByCV(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Tap by CV failed: %s", err.Error())), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Tap by CV failed: %s", err.Error())), nil
|
||||||
}
|
}
|
||||||
@@ -499,6 +579,10 @@ func (t *ToolTapByCV) ConvertActionToCallToolRequest(action MobileAction) (mcp.C
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"imagePath": "", // Will be handled by the tool based on UI types
|
"imagePath": "", // Will be handled by the tool based on UI types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,9 +1086,25 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if swipeAppReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if swipeAppReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(swipeAppReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
if swipeAppReq.Index > 0 {
|
||||||
|
opts = append(opts, option.WithIndex(swipeAppReq.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// Swipe to tap app action logic
|
// Swipe to tap app action logic
|
||||||
log.Info().Str("appName", swipeAppReq.AppName).Msg("swipe to tap app")
|
log.Info().Str("appName", swipeAppReq.AppName).Msg("swipe to tap app")
|
||||||
err = driverExt.SwipeToTapApp(swipeAppReq.AppName)
|
err = driverExt.SwipeToTapApp(swipeAppReq.AppName, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap app failed: %s", err.Error())), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap app failed: %s", err.Error())), nil
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1118,10 @@ func (t *ToolSwipeToTapApp) ConvertActionToCallToolRequest(action MobileAction)
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"appName": appName,
|
"appName": appName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params)
|
||||||
@@ -1050,9 +1154,28 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if swipeTextReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
if swipeTextReq.Regex {
|
||||||
|
opts = append(opts, option.WithRegex(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if swipeTextReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(swipeTextReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
if swipeTextReq.Index > 0 {
|
||||||
|
opts = append(opts, option.WithIndex(swipeTextReq.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// Swipe to tap text action logic
|
// Swipe to tap text action logic
|
||||||
log.Info().Str("text", swipeTextReq.Text).Msg("swipe to tap text")
|
log.Info().Str("text", swipeTextReq.Text).Msg("swipe to tap text")
|
||||||
err = driverExt.SwipeToTapTexts([]string{swipeTextReq.Text})
|
err = driverExt.SwipeToTapTexts([]string{swipeTextReq.Text}, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap text failed: %s", err.Error())), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap text failed: %s", err.Error())), nil
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1189,10 @@ func (t *ToolSwipeToTapText) ConvertActionToCallToolRequest(action MobileAction)
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"text": text,
|
"text": text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params)
|
||||||
@@ -1098,9 +1225,28 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("parse parameters error: %w", err)
|
return nil, fmt.Errorf("parse parameters error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build action options from request structure
|
||||||
|
var opts []option.ActionOption
|
||||||
|
|
||||||
|
// Add boolean options
|
||||||
|
if swipeTextsReq.IgnoreNotFoundError {
|
||||||
|
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||||
|
}
|
||||||
|
if swipeTextsReq.Regex {
|
||||||
|
opts = append(opts, option.WithRegex(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options
|
||||||
|
if swipeTextsReq.MaxRetryTimes > 0 {
|
||||||
|
opts = append(opts, option.WithMaxRetryTimes(swipeTextsReq.MaxRetryTimes))
|
||||||
|
}
|
||||||
|
if swipeTextsReq.Index > 0 {
|
||||||
|
opts = append(opts, option.WithIndex(swipeTextsReq.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// Swipe to tap texts action logic
|
// Swipe to tap texts action logic
|
||||||
log.Info().Strs("texts", swipeTextsReq.Texts).Msg("swipe to tap texts")
|
log.Info().Strs("texts", swipeTextsReq.Texts).Msg("swipe to tap texts")
|
||||||
err = driverExt.SwipeToTapTexts(swipeTextsReq.Texts)
|
err = driverExt.SwipeToTapTexts(swipeTextsReq.Texts, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap texts failed: %s", err.Error())), nil
|
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap texts failed: %s", err.Error())), nil
|
||||||
}
|
}
|
||||||
@@ -1121,6 +1267,10 @@ func (t *ToolSwipeToTapTexts) ConvertActionToCallToolRequest(action MobileAction
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"texts": texts,
|
"texts": texts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract options to arguments
|
||||||
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,6 +1348,48 @@ func mapToStruct(m map[string]any, out interface{}) error {
|
|||||||
return json.Unmarshal(b, out)
|
return json.Unmarshal(b, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractActionOptionsToArguments extracts action options and adds them to arguments map
|
||||||
|
// This is a generic helper that can be used by multiple tools
|
||||||
|
func extractActionOptionsToArguments(actionOptions []option.ActionOption, arguments map[string]any) {
|
||||||
|
if len(actionOptions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all options to a temporary ActionOptions to extract values
|
||||||
|
tempOptions := &option.ActionOptions{}
|
||||||
|
for _, opt := range actionOptions {
|
||||||
|
opt(tempOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define option mappings for common boolean options
|
||||||
|
booleanOptions := map[string]bool{
|
||||||
|
"ignore_NotFoundError": tempOptions.IgnoreNotFoundError,
|
||||||
|
"regex": tempOptions.Regex,
|
||||||
|
"tap_random_rect": tempOptions.TapRandomRect,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add boolean options only if they are true
|
||||||
|
for key, value := range booleanOptions {
|
||||||
|
if value {
|
||||||
|
arguments[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numeric options only if they have meaningful values
|
||||||
|
if tempOptions.MaxRetryTimes > 0 {
|
||||||
|
arguments["max_retry_times"] = tempOptions.MaxRetryTimes
|
||||||
|
}
|
||||||
|
if tempOptions.Index != 0 {
|
||||||
|
arguments["index"] = tempOptions.Index
|
||||||
|
}
|
||||||
|
if tempOptions.Duration > 0 {
|
||||||
|
arguments["duration"] = tempOptions.Duration
|
||||||
|
}
|
||||||
|
if tempOptions.PressDuration > 0 {
|
||||||
|
arguments["press_duration"] = tempOptions.PressDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ToolHome implements the home tool call.
|
// ToolHome implements the home tool call.
|
||||||
type ToolHome struct{}
|
type ToolHome struct{}
|
||||||
|
|
||||||
|
|||||||
+129
-18
@@ -3,6 +3,7 @@ package uixt
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,19 +19,43 @@ func TestNewMCPServer(t *testing.T) {
|
|||||||
expectedTools := []string{
|
expectedTools := []string{
|
||||||
"list_available_devices",
|
"list_available_devices",
|
||||||
"select_device",
|
"select_device",
|
||||||
|
"tap_xy",
|
||||||
|
"tap_abs_xy",
|
||||||
|
"tap_ocr",
|
||||||
|
"tap_cv",
|
||||||
|
"double_tap_xy",
|
||||||
|
"swipe_direction",
|
||||||
|
"swipe_coordinate",
|
||||||
|
"swipe_to_tap_app",
|
||||||
|
"swipe_to_tap_text",
|
||||||
|
"swipe_to_tap_texts",
|
||||||
|
"drag",
|
||||||
|
"input",
|
||||||
|
"screenshot",
|
||||||
|
"get_screen_size",
|
||||||
|
"press_button",
|
||||||
|
"home",
|
||||||
|
"back",
|
||||||
"list_packages",
|
"list_packages",
|
||||||
"app_launch",
|
"app_launch",
|
||||||
"app_terminate",
|
"app_terminate",
|
||||||
"get_screen_size",
|
"app_install",
|
||||||
"press_button",
|
"app_uninstall",
|
||||||
"tap_xy",
|
"app_clear",
|
||||||
"swipe",
|
|
||||||
"drag",
|
|
||||||
"screenshot",
|
|
||||||
"home",
|
|
||||||
"back",
|
|
||||||
"input",
|
|
||||||
"sleep",
|
"sleep",
|
||||||
|
"sleep_ms",
|
||||||
|
"sleep_random",
|
||||||
|
"set_ime",
|
||||||
|
"get_source",
|
||||||
|
"close_popups",
|
||||||
|
"web_login_none_ui",
|
||||||
|
"secondary_click",
|
||||||
|
"hover_by_selector",
|
||||||
|
"tap_by_selector",
|
||||||
|
"secondary_click_by_selector",
|
||||||
|
"web_close_tab",
|
||||||
|
"ai_action",
|
||||||
|
"finished",
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredToolNames := make(map[string]bool)
|
registeredToolNames := make(map[string]bool)
|
||||||
@@ -48,19 +73,43 @@ func TestToolInterfaces(t *testing.T) {
|
|||||||
tools := []ActionTool{
|
tools := []ActionTool{
|
||||||
&ToolListAvailableDevices{},
|
&ToolListAvailableDevices{},
|
||||||
&ToolSelectDevice{},
|
&ToolSelectDevice{},
|
||||||
|
&ToolTapXY{},
|
||||||
|
&ToolTapAbsXY{},
|
||||||
|
&ToolTapByOCR{},
|
||||||
|
&ToolTapByCV{},
|
||||||
|
&ToolDoubleTapXY{},
|
||||||
|
&ToolSwipeDirection{},
|
||||||
|
&ToolSwipeCoordinate{},
|
||||||
|
&ToolSwipeToTapApp{},
|
||||||
|
&ToolSwipeToTapText{},
|
||||||
|
&ToolSwipeToTapTexts{},
|
||||||
|
&ToolDrag{},
|
||||||
|
&ToolInput{},
|
||||||
|
&ToolScreenShot{},
|
||||||
|
&ToolGetScreenSize{},
|
||||||
|
&ToolPressButton{},
|
||||||
|
&ToolHome{},
|
||||||
|
&ToolBack{},
|
||||||
&ToolListPackages{},
|
&ToolListPackages{},
|
||||||
&ToolLaunchApp{},
|
&ToolLaunchApp{},
|
||||||
&ToolTerminateApp{},
|
&ToolTerminateApp{},
|
||||||
&ToolGetScreenSize{},
|
&ToolAppInstall{},
|
||||||
&ToolPressButton{},
|
&ToolAppUninstall{},
|
||||||
&ToolTapXY{},
|
&ToolAppClear{},
|
||||||
&ToolSwipeDirection{},
|
|
||||||
&ToolDrag{},
|
|
||||||
&ToolScreenShot{},
|
|
||||||
&ToolHome{},
|
|
||||||
&ToolBack{},
|
|
||||||
&ToolInput{},
|
|
||||||
&ToolSleep{},
|
&ToolSleep{},
|
||||||
|
&ToolSleepMS{},
|
||||||
|
&ToolSleepRandom{},
|
||||||
|
&ToolSetIme{},
|
||||||
|
&ToolGetSource{},
|
||||||
|
&ToolClosePopups{},
|
||||||
|
&ToolWebLoginNoneUI{},
|
||||||
|
&ToolSecondaryClick{},
|
||||||
|
&ToolHoverBySelector{},
|
||||||
|
&ToolTapBySelector{},
|
||||||
|
&ToolSecondaryClickBySelector{},
|
||||||
|
&ToolWebCloseTab{},
|
||||||
|
&ToolAIAction{},
|
||||||
|
&ToolFinished{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
@@ -70,3 +119,65 @@ func TestToolInterfaces(t *testing.T) {
|
|||||||
assert.NotNil(t, tool.Implement(), "Tool implementation should not be nil")
|
assert.NotNil(t, tool.Implement(), "Tool implementation should not be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIgnoreNotFoundErrorOption(t *testing.T) {
|
||||||
|
// Test that ignore_NotFoundError option is properly extracted and applied
|
||||||
|
server := NewMCPServer()
|
||||||
|
|
||||||
|
// Test TapByOCR tool
|
||||||
|
tapOCRTool := server.GetToolByAction(option.ACTION_TapByOCR)
|
||||||
|
assert.NotNil(t, tapOCRTool, "TapByOCR tool should be available")
|
||||||
|
|
||||||
|
// Create a mock action with ignore_NotFoundError option
|
||||||
|
actionOptions := option.NewActionOptions(
|
||||||
|
option.WithIgnoreNotFoundError(true),
|
||||||
|
option.WithMaxRetryTimes(2),
|
||||||
|
option.WithIndex(1),
|
||||||
|
option.WithRegex(true),
|
||||||
|
option.WithTapRandomRect(true),
|
||||||
|
)
|
||||||
|
action := MobileAction{
|
||||||
|
Method: option.ACTION_TapByOCR,
|
||||||
|
Params: "test_text",
|
||||||
|
ActionOptions: *actionOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert action to MCP call tool request
|
||||||
|
request, err := tapOCRTool.ConvertActionToCallToolRequest(action)
|
||||||
|
assert.NoError(t, err, "Should convert action to request without error")
|
||||||
|
|
||||||
|
// Verify that ignore_NotFoundError option is included in arguments
|
||||||
|
args := request.Params.Arguments
|
||||||
|
assert.Equal(t, true, args["ignore_NotFoundError"], "ignore_NotFoundError should be true")
|
||||||
|
assert.Equal(t, 2, args["max_retry_times"], "max_retry_times should be 2")
|
||||||
|
assert.Equal(t, 1, args["index"], "index should be 1")
|
||||||
|
assert.Equal(t, true, args["regex"], "regex should be true")
|
||||||
|
assert.Equal(t, true, args["tap_random_rect"], "tap_random_rect should be true")
|
||||||
|
assert.Equal(t, "test_text", args["text"], "text should be test_text")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractActionOptionsToArguments(t *testing.T) {
|
||||||
|
// Test the extractActionOptionsToArguments helper function
|
||||||
|
actionOptions := []option.ActionOption{
|
||||||
|
option.WithIgnoreNotFoundError(true),
|
||||||
|
option.WithMaxRetryTimes(3),
|
||||||
|
option.WithIndex(2),
|
||||||
|
option.WithRegex(true),
|
||||||
|
option.WithTapRandomRect(false), // false should not be included
|
||||||
|
option.WithDuration(1.5),
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments := make(map[string]any)
|
||||||
|
extractActionOptionsToArguments(actionOptions, arguments)
|
||||||
|
|
||||||
|
// Verify extracted options
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|||||||
+39
-13
@@ -16,9 +16,11 @@ type TargetDeviceRequest struct {
|
|||||||
|
|
||||||
type TapRequest struct {
|
type TapRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
|
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DragRequest struct {
|
type DragRequest struct {
|
||||||
@@ -103,17 +105,28 @@ type WebLoginNoneUIRequest struct {
|
|||||||
|
|
||||||
type SwipeToTapAppRequest struct {
|
type SwipeToTapAppRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
|
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the app"`
|
||||||
|
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SwipeToTapTextRequest struct {
|
type SwipeToTapTextRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
|
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"`
|
||||||
|
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
|
||||||
|
Regex bool `json:"regex" desc:"Use regex to match text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SwipeToTapTextsRequest struct {
|
type SwipeToTapTextsRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
|
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the texts"`
|
||||||
|
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
|
||||||
|
Regex bool `json:"regex" desc:"Use regex to match text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecondaryClickRequest struct {
|
type SecondaryClickRequest struct {
|
||||||
@@ -144,25 +157,38 @@ type GetSourceRequest struct {
|
|||||||
|
|
||||||
type TapAbsXYRequest struct {
|
type TapAbsXYRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
|
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
|
||||||
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
|
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
|
||||||
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
|
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TapByOCRRequest struct {
|
type TapByOCRRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
|
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"`
|
||||||
|
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
|
||||||
|
Regex bool `json:"regex" desc:"Use regex to match text"`
|
||||||
|
TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in text rectangle"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TapByCVRequest struct {
|
type TapByCVRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
|
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the image"`
|
||||||
|
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
|
||||||
|
TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in image rectangle"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DoubleTapXYRequest struct {
|
type DoubleTapXYRequest struct {
|
||||||
TargetDeviceRequest
|
TargetDeviceRequest
|
||||||
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
|
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SwipeAdvancedRequest struct {
|
type SwipeAdvancedRequest struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user