mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-27 02:21:23 +08:00
Merge branch 'mcp-plugin' into 'master'
CaseRunner 支持注册自定义 driver See merge request iesqa/httprunner!86
This commit is contained in:
@@ -151,9 +151,6 @@ func convertCompatMobileStep(mobileUI *MobileUI) {
|
||||
if ma.Method == uixt.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
||||
ma.ActionOptions.MaxRetryTimes = 10
|
||||
}
|
||||
if ma.Method == uixt.ACTION_Swipe {
|
||||
ma.ActionOptions.Direction = ma.Params
|
||||
}
|
||||
mobileUI.Actions[i] = ma
|
||||
}
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/maja42/goval v1.2.1
|
||||
github.com/mark3labs/mcp-go v0.22.0
|
||||
github.com/mark3labs/mcp-go v0.27.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -177,8 +177,8 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
|
||||
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
|
||||
github.com/mark3labs/mcp-go v0.22.0 h1:cCEBWi4Yy9Kio+OW1hWIyi4WLsSr+RBBK6FI5tj+b7I=
|
||||
github.com/mark3labs/mcp-go v0.22.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
|
||||
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
||||
@@ -195,12 +195,12 @@ func Interface2Float64(i interface{}) (float64, error) {
|
||||
return float64(v), nil
|
||||
case float64:
|
||||
return v, nil
|
||||
case string:
|
||||
case string: // e.g. "1", "0.5"
|
||||
floatVar, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return floatVar, err
|
||||
return floatVar, nil
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
|
||||
96
internal/builtin/utils_test.go
Normal file
96
internal/builtin/utils_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInterface2Float64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
want float64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "convert int",
|
||||
input: 42,
|
||||
want: 42.0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert int32",
|
||||
input: int32(42),
|
||||
want: 42.0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert int64",
|
||||
input: int64(42),
|
||||
want: 42.0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert float32",
|
||||
input: float32(42.5),
|
||||
want: 42.5,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert float64",
|
||||
input: 42.5,
|
||||
want: 42.5,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert string valid number",
|
||||
input: "42.5",
|
||||
want: 42.5,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert string valid number",
|
||||
input: "425",
|
||||
want: 425.0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert string invalid number",
|
||||
input: "invalid",
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "convert json.Number valid",
|
||||
input: json.Number("42.5"),
|
||||
want: 42.5,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "convert json.Number invalid",
|
||||
input: json.Number("invalid"),
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "convert unsupported type",
|
||||
input: []int{1, 2, 3},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Interface2Float64(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,7 @@ func (h *MCPHub) connectToServer(ctx context.Context, serverName string, config
|
||||
switch config.TransportType {
|
||||
case "sse":
|
||||
mcpClient, err = client.NewSSEMCPClient(config.URL)
|
||||
|
||||
case "stdio", "": // default to stdio
|
||||
var env []string
|
||||
for k, v := range config.Env {
|
||||
@@ -108,6 +109,17 @@ func (h *MCPHub) connectToServer(ctx context.Context, serverName string, config
|
||||
}
|
||||
mcpClient, err = client.NewStdioMCPClient(config.Command,
|
||||
env, config.Args...)
|
||||
|
||||
// print MCP Server logs for stdio transport
|
||||
stderr, _ := client.GetStderr(mcpClient)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
fmt.Fprintf(os.Stderr, "MCP Server %s: %s\n",
|
||||
serverName, scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported transport type: %s", config.TransportType)
|
||||
}
|
||||
@@ -115,16 +127,6 @@ func (h *MCPHub) connectToServer(ctx context.Context, serverName string, config
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
// print MCP Server logs
|
||||
stderr := client.GetStderr(mcpClient)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
fmt.Fprintf(os.Stderr, "MCP Server %s: %s\n",
|
||||
serverName, scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
// prepare client init request
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-beta-2505091122
|
||||
v5.0.0-beta-2505141501
|
||||
|
||||
88
runner.go
88
runner.go
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
@@ -228,7 +229,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
|
||||
// run testcase one by one
|
||||
for _, testcase := range testCases {
|
||||
// each testcase has its own case runner
|
||||
caseRunner, err := r.NewCaseRunner(*testcase)
|
||||
caseRunner, err := NewCaseRunner(*testcase, r)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[Run] init case runner failed")
|
||||
return err
|
||||
@@ -278,10 +279,14 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
|
||||
|
||||
// NewCaseRunner creates a new case runner for testcase.
|
||||
// each testcase has its own case runner
|
||||
func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
// If the provided hrpRunner is nil, a default HRPRunner will be created and used.
|
||||
func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error) {
|
||||
if hrpRunner == nil {
|
||||
hrpRunner = NewRunner(nil)
|
||||
}
|
||||
caseRunner := &CaseRunner{
|
||||
TestCase: testcase,
|
||||
hrpRunner: r,
|
||||
hrpRunner: hrpRunner,
|
||||
parser: NewParser(),
|
||||
uixtDrivers: make(map[string]*uixt.XTDriver),
|
||||
}
|
||||
@@ -289,7 +294,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
|
||||
// init parser plugin
|
||||
if config.PluginSetting != nil {
|
||||
plugin, err := initPlugin(config.Path, r.venv, r.pluginLogOn)
|
||||
plugin, err := initPlugin(config.Path, hrpRunner.venv, hrpRunner.pluginLogOn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init plugin failed")
|
||||
}
|
||||
@@ -317,12 +322,12 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
}
|
||||
|
||||
// set request timeout in seconds
|
||||
if config.RequestTimeout != 0 {
|
||||
r.SetRequestTimeout(config.RequestTimeout)
|
||||
if parsedConfig.RequestTimeout != 0 {
|
||||
hrpRunner.SetRequestTimeout(parsedConfig.RequestTimeout)
|
||||
}
|
||||
// set testcase timeout in seconds
|
||||
if config.CaseTimeout != 0 {
|
||||
r.SetCaseTimeout(config.CaseTimeout)
|
||||
if parsedConfig.CaseTimeout != 0 {
|
||||
hrpRunner.SetCaseTimeout(parsedConfig.CaseTimeout)
|
||||
}
|
||||
|
||||
caseRunner.TestCase.Config = parsedConfig
|
||||
@@ -450,7 +455,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init android XTDriver failed")
|
||||
}
|
||||
r.uixtDrivers[androidDeviceOptions.SerialNumber] = driverExt
|
||||
r.RegisterUIXTDriver(androidDeviceOptions.SerialNumber, driverExt)
|
||||
}
|
||||
// parse iOS devices config
|
||||
for _, iosDeviceOptions := range parsedConfig.IOS {
|
||||
@@ -473,7 +478,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init ios XTDriver failed")
|
||||
}
|
||||
r.uixtDrivers[iosDeviceOptions.UDID] = driverExt
|
||||
r.RegisterUIXTDriver(iosDeviceOptions.UDID, driverExt)
|
||||
}
|
||||
// parse harmony devices config
|
||||
for _, harmonyDeviceOptions := range parsedConfig.Harmony {
|
||||
@@ -496,7 +501,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init harmony XTDriver failed")
|
||||
}
|
||||
r.uixtDrivers[harmonyDeviceOptions.ConnectKey] = driverExt
|
||||
r.RegisterUIXTDriver(harmonyDeviceOptions.ConnectKey, driverExt)
|
||||
}
|
||||
// parse browser devices config
|
||||
for _, browserDeviceOptions := range parsedConfig.Browser {
|
||||
@@ -509,26 +514,24 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init browser device failed")
|
||||
}
|
||||
if err := device.Setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driver, err := device.NewDriver()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := driver.Setup(); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "init browser driver failed")
|
||||
}
|
||||
driverExt, err := uixt.NewXTDriver(driver, aiOpts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init browser XTDriver failed")
|
||||
}
|
||||
r.uixtDrivers[browserDeviceOptions.BrowserID] = driverExt
|
||||
r.RegisterUIXTDriver(browserDeviceOptions.BrowserID, driverExt)
|
||||
}
|
||||
|
||||
return parsedConfig, nil
|
||||
}
|
||||
|
||||
func (r *CaseRunner) RegisterUIXTDriver(serial string, driver *uixt.XTDriver) {
|
||||
r.uixtDrivers[serial] = driver
|
||||
}
|
||||
|
||||
func (r *CaseRunner) parseDeviceConfig(device interface{}, configVariables map[string]interface{}) error {
|
||||
deviceValue := reflect.ValueOf(device).Elem()
|
||||
deviceType := deviceValue.Type()
|
||||
@@ -705,14 +708,9 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
|
||||
log.Info().Str("step", stepName).Str("type", stepType).Msg("run step start")
|
||||
|
||||
// run times of step
|
||||
loopTimes := step.Config().Loops
|
||||
if loopTimes < 0 {
|
||||
log.Warn().Int("loops", loopTimes).Msg("loop times should be positive, set to 1")
|
||||
loopTimes = 1
|
||||
} else if loopTimes == 0 {
|
||||
loopTimes = 1
|
||||
} else if loopTimes > 1 {
|
||||
log.Info().Int("loops", loopTimes).Msg("run step with specified loop times")
|
||||
loopTimes, err := r.getLoopTimes(step)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get loop times")
|
||||
}
|
||||
|
||||
// run step with specified loop times
|
||||
@@ -838,3 +836,39 @@ func (r *SessionRunner) GetSessionVariables() map[string]interface{} {
|
||||
func (r *SessionRunner) GetTransactions() map[string]map[TransactionType]time.Time {
|
||||
return r.transactions
|
||||
}
|
||||
|
||||
func (r *SessionRunner) getLoopTimes(step IStep) (int, error) {
|
||||
loops := step.Config().Loops
|
||||
if loops == nil {
|
||||
// default run once
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
loopTimes, err := loops.Value()
|
||||
if err != nil {
|
||||
parsed, err := r.caseRunner.parser.ParseString(
|
||||
*loops.StringValue, step.Config().Variables)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to parse loop times")
|
||||
}
|
||||
switch v := parsed.(type) {
|
||||
case int:
|
||||
loopTimes = v
|
||||
case string:
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to parse loop times")
|
||||
}
|
||||
loopTimes = n
|
||||
}
|
||||
}
|
||||
if loopTimes < 0 {
|
||||
return 0, fmt.Errorf("loop times should be positive, got %d", loopTimes)
|
||||
} else if loopTimes == 0 {
|
||||
loopTimes = 1
|
||||
} else if loopTimes > 1 {
|
||||
log.Info().Int("loops", loopTimes).Msg("set multiple loop times")
|
||||
}
|
||||
|
||||
return loopTimes, nil
|
||||
}
|
||||
|
||||
7
step.go
7
step.go
@@ -1,6 +1,9 @@
|
||||
package hrp
|
||||
|
||||
import "github.com/httprunner/httprunner/v5/uixt"
|
||||
import (
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
|
||||
type StepType string
|
||||
|
||||
@@ -31,7 +34,7 @@ type StepConfig struct {
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Loops int `json:"loops,omitempty" yaml:"loops,omitempty"`
|
||||
Loops *types.IntOrString `json:"loops,omitempty" yaml:"loops,omitempty"`
|
||||
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/internal/httpstat"
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
|
||||
type HTTPMethod string
|
||||
@@ -559,7 +560,9 @@ func (s *StepRequest) HTTP2() *StepRequest {
|
||||
|
||||
// Loop specify running times for the current step
|
||||
func (s *StepRequest) Loop(times int) *StepRequest {
|
||||
s.Loops = times
|
||||
s.Loops = &types.IntOrString{
|
||||
IntValue: ×,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe
|
||||
// merge & override extractors
|
||||
config.Export = mergeSlices(s.StepExport, config.Export)
|
||||
|
||||
caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(*copiedTestCase)
|
||||
caseRunner, err := NewCaseRunner(*copiedTestCase, r.caseRunner.hrpRunner)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create case runner failed")
|
||||
return stepResult, err
|
||||
|
||||
@@ -447,12 +447,12 @@ func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepMobile) Call(name string, fn func()) *StepMobile {
|
||||
func (s *StepMobile) Call(name string, fn func(), opts ...option.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_CallFunction,
|
||||
Params: name, // function description
|
||||
Fn: fn,
|
||||
Options: nil,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ func TestSessionRunner(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
caseRunner, _ := hrp.NewRunner(t).NewCaseRunner(testcase)
|
||||
caseRunner, _ := hrp.NewCaseRunner(testcase, hrp.NewRunner(t))
|
||||
sessionRunner := caseRunner.NewSession()
|
||||
step := testcase.TestSteps[0]
|
||||
if !assert.Equal(t, step.Config().Variables["varFoo"], "${max($a, $b)}") {
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestRunRequestStatOn(t *testing.T) {
|
||||
Config: hrp.NewConfig("test").SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []hrp.IStep{stepGET, stepPOSTData},
|
||||
}
|
||||
caseRunner, _ := hrp.NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase)
|
||||
caseRunner, _ := hrp.NewCaseRunner(testcase, hrp.NewRunner(t).SetHTTPStatOn())
|
||||
sessionRunner := caseRunner.NewSession()
|
||||
summary, err := sessionRunner.Start(nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -307,11 +307,12 @@ func (ad *ADBDriver) TapXY(x, y float64, opts ...option.ActionOption) error {
|
||||
|
||||
func (ad *ADBDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("x", x).Float64("y", y).Msg("ADBDriver.TapAbsXY")
|
||||
var err error
|
||||
x, y, err = handlerTapAbsXY(ad, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_TapAbsXY(ad, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ad, ACTION_TapAbsXY, actionOptions)
|
||||
|
||||
// adb shell input tap x y
|
||||
xStr := fmt.Sprintf("%.1f", x)
|
||||
@@ -325,11 +326,12 @@ func (ad *ADBDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
|
||||
func (ad *ADBDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("x", x).Float64("y", y).Msg("ADBDriver.DoubleTap")
|
||||
var err error
|
||||
x, y, err = handlerDoubleTap(ad, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_DoubleTap(ad, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ad, ACTION_DoubleTapXY, actionOptions)
|
||||
|
||||
// adb shell input tap x y
|
||||
xStr := fmt.Sprintf("%.1f", x)
|
||||
@@ -373,12 +375,13 @@ func (ad *ADBDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO
|
||||
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
|
||||
Float64("toX", toX).Float64("toY", toY).Msg("ADBDriver.Drag")
|
||||
|
||||
fromX, fromY, toX, toY, err = handlerDrag(ad, fromX, fromY, toX, toY, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err = preHandler_Drag(ad, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ad, ACTION_Drag, actionOptions)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
duration := 200.0
|
||||
if actionOptions.Duration > 0 {
|
||||
duration = actionOptions.Duration * 1000
|
||||
@@ -403,11 +406,13 @@ func (ad *ADBDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO
|
||||
func (ad *ADBDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
|
||||
Float64("toX", toX).Float64("toY", toY).Msg("ADBDriver.Swipe")
|
||||
var err error
|
||||
fromX, fromY, toX, toY, err = handlerSwipe(ad, fromX, fromY, toX, toY)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err := preHandler_Swipe(ad, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ad, ACTION_Swipe, actionOptions)
|
||||
|
||||
// adb shell input swipe fromX fromY toX toY
|
||||
_, err = ad.runShellCommand(
|
||||
|
||||
@@ -257,11 +257,12 @@ func (ud *UIA2Driver) Orientation() (orientation types.Orientation, err error) {
|
||||
|
||||
func (ud *UIA2Driver) DoubleTap(x, y float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.DoubleTap")
|
||||
var err error
|
||||
x, y, err = handlerDoubleTap(ud, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_DoubleTap(ud, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ud, ACTION_DoubleTapXY, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
@@ -298,14 +299,13 @@ func (ud *UIA2Driver) TapXY(x, y float64, opts ...option.ActionOption) error {
|
||||
func (ud *UIA2Driver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.TapAbsXY")
|
||||
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
|
||||
|
||||
var err error
|
||||
x, y, err = handlerTapAbsXY(ud, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_TapAbsXY(ud, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ud, ACTION_TapAbsXY, actionOptions)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
duration := 100.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration * 1000 // convert to ms
|
||||
@@ -362,11 +362,12 @@ func (ud *UIA2Driver) Drag(fromX, fromY, toX, toY float64, opts ...option.Action
|
||||
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
|
||||
Float64("toX", toX).Float64("toY", toY).Msg("UIA2Driver.Drag")
|
||||
|
||||
var err error
|
||||
fromX, fromY, toX, toY, err = handlerDrag(ud, fromX, fromY, toX, toY, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err := preHandler_Drag(ud, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ud, ACTION_Drag, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"startX": fromX,
|
||||
@@ -391,12 +392,14 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio
|
||||
// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
|
||||
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
|
||||
Float64("toX", toX).Float64("toY", toY).Msg("UIA2Driver.Swipe")
|
||||
var err error
|
||||
fromX, fromY, toX, toY, err = handlerSwipe(ud, fromX, fromY, toX, toY)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err := preHandler_Swipe(ud, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
defer postHandler(ud, ACTION_Swipe, actionOptions)
|
||||
|
||||
duration := 200.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration * 1000 // ms
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -133,6 +134,23 @@ func TestDriver_ADB_TapXY(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestDriver_ADB_TapXY_WithHook(t *testing.T) {
|
||||
driver := setupADBDriverExt(t)
|
||||
|
||||
err := driver.Call("pre hook", func() {
|
||||
log.Info().Msg("pre hook")
|
||||
}, option.WithTimeout(1))
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = driver.TapXY(0.4, 0.5)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = driver.Call("post hook", func() {
|
||||
log.Info().Msg("post hook")
|
||||
}, option.WithTimeout(1))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestDriver_ADB_TapAbsXY(t *testing.T) {
|
||||
driver := setupADBDriverExt(t)
|
||||
err := driver.TapAbsXY(100, 300)
|
||||
|
||||
@@ -114,17 +114,19 @@ func (wd *BrowserDriver) Setup() error {
|
||||
}
|
||||
|
||||
func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option.ActionOption) (err error) {
|
||||
fromX, fromY, toX, toY, err = handlerDrag(wd, fromX, fromY, toX, toY, options...)
|
||||
actionOptions := option.NewActionOptions(options...)
|
||||
fromX, fromY, toX, toY, err = preHandler_Drag(wd, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_Drag, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"from_x": fromX,
|
||||
"from_y": fromY,
|
||||
"to_x": toX,
|
||||
"to_y": toY,
|
||||
}
|
||||
actionOptions := option.NewActionOptions(options...)
|
||||
|
||||
if actionOptions.Duration > 0 {
|
||||
data["duration"] = actionOptions.Duration
|
||||
@@ -511,13 +513,13 @@ func (wd *BrowserDriver) Tap(x, y float64, options ...option.ActionOption) error
|
||||
}
|
||||
|
||||
func (wd *BrowserDriver) TapFloat(x, y float64, opts ...option.ActionOption) error {
|
||||
var err error
|
||||
x, y, err = handlerTapAbsXY(wd, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_TapAbsXY(wd, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_TapAbsXY, actionOptions)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
duration := 0.1
|
||||
if actionOptions.Duration > 0 {
|
||||
duration = actionOptions.Duration
|
||||
@@ -535,11 +537,13 @@ func (wd *BrowserDriver) TapFloat(x, y float64, opts ...option.ActionOption) err
|
||||
|
||||
// DoubleTap Sends a double tap event at the coordinate.
|
||||
func (wd *BrowserDriver) DoubleTap(x, y float64, options ...option.ActionOption) error {
|
||||
var err error
|
||||
x, y, err = handlerDoubleTap(wd, x, y, options...)
|
||||
actionOptions := option.NewActionOptions(options...)
|
||||
x, y, err := preHandler_DoubleTap(wd, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_DoubleTapXY, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
|
||||
@@ -87,7 +87,7 @@ const (
|
||||
type MobileAction struct {
|
||||
Method ActionMethod `json:"method,omitempty" yaml:"method,omitempty"`
|
||||
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||
Fn func() `json:"-" yaml:"-"` // only used for function action, not serialized
|
||||
Fn func() `json:"-" yaml:"-"` // used for function action, not serialized
|
||||
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
option.ActionOptions
|
||||
}
|
||||
@@ -306,6 +306,13 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
} else if sd, ok := action.Params.(SleepConfig); ok {
|
||||
sleepStrict(sd.StartTime, int64(sd.Seconds*1000))
|
||||
return nil
|
||||
} else if param, ok := action.Params.(string); ok {
|
||||
seconds, err := builtin.ConvertToFloat64(param)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid sleep params: %v(%T)", action.Params, action.Params)
|
||||
}
|
||||
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
|
||||
case ACTION_SleepMS:
|
||||
@@ -335,9 +342,10 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
case ACTION_ClosePopups:
|
||||
return dExt.ClosePopupsHandler()
|
||||
case ACTION_CallFunction:
|
||||
fn := action.Fn
|
||||
fn()
|
||||
return nil
|
||||
if funcDesc, ok := action.Params.(string); ok {
|
||||
return dExt.Call(funcDesc, action.Fn, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid function description: %v", action.Params)
|
||||
case ACTION_AIAction:
|
||||
if prompt, ok := action.Params.(string); ok {
|
||||
return dExt.AIAction(prompt, action.GetOptions()...)
|
||||
|
||||
@@ -60,7 +60,7 @@ func (dExt *XTDriver) PlanNextAction(text string, opts ...option.ActionOption) (
|
||||
return nil, errors.New("LLM service is not initialized")
|
||||
}
|
||||
|
||||
compressedBufSource, err := dExt.GetScreenShotBuffer()
|
||||
compressedBufSource, err := getScreenShotBuffer(dExt.IDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) er
|
||||
return errors.New("LLM service is not initialized")
|
||||
}
|
||||
|
||||
compressedBufSource, err := dExt.GetScreenShotBuffer()
|
||||
compressedBufSource, err := getScreenShotBuffer(dExt.IDriver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,28 +48,10 @@ func (s *ScreenResult) FilterTextsByScope(x1, y1, x2, y2 float64) ai.OCRTexts {
|
||||
})
|
||||
}
|
||||
|
||||
func (dExt *XTDriver) GetScreenShotBuffer() (compressedBufSource *bytes.Buffer, err error) {
|
||||
// take screenshot
|
||||
bufSource, err := dExt.ScreenShot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||
"take screenshot failed %v", err)
|
||||
}
|
||||
|
||||
// compress screenshot
|
||||
compressBufSource, err := compressImageBuffer(bufSource)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||
"compress screenshot failed %v", err)
|
||||
}
|
||||
|
||||
return compressBufSource, nil
|
||||
}
|
||||
|
||||
// GetScreenResult takes a screenshot, returns the image recognition result
|
||||
func (dExt *XTDriver) GetScreenResult(opts ...option.ActionOption) (screenResult *ScreenResult, err error) {
|
||||
// get compressed screenshot buffer
|
||||
compressBufSource, err := dExt.GetScreenShotBuffer()
|
||||
compressBufSource, err := getScreenShotBuffer(dExt.IDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -220,6 +202,25 @@ func (dExt *XTDriver) FindUIResult(opts ...option.ActionOption) (uiResult ai.UIR
|
||||
return
|
||||
}
|
||||
|
||||
// getScreenShotBuffer takes a screenshot, returns the compressed image buffer
|
||||
func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err error) {
|
||||
// take screenshot
|
||||
bufSource, err := driver.ScreenShot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||
"take screenshot failed %v", err)
|
||||
}
|
||||
|
||||
// compress screenshot
|
||||
compressBufSource, err := compressImageBuffer(bufSource)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||
"compress screenshot failed %v", err)
|
||||
}
|
||||
|
||||
return compressBufSource, nil
|
||||
}
|
||||
|
||||
// saveScreenShot saves compressed image file with file name
|
||||
func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
|
||||
// notice: screenshot data is a stream, so we need to copy it to a new buffer
|
||||
@@ -314,17 +315,16 @@ func MarkUIOperation(driver IDriver, actionType ActionMethod, actionCoordinates
|
||||
}
|
||||
|
||||
// create screenshot save path
|
||||
timestamp := builtin.GenNameWithTimestamp("action_%d")
|
||||
var imagePath string
|
||||
timestamp := builtin.GenNameWithTimestamp("%d")
|
||||
imagePath := filepath.Join(
|
||||
config.GetConfig().ScreenShotsPath,
|
||||
fmt.Sprintf("action_%s_pre_%s.png", timestamp, actionType),
|
||||
)
|
||||
|
||||
if actionType == ACTION_TapAbsXY || actionType == ACTION_DoubleTapXY {
|
||||
if len(actionCoordinates) != 2 {
|
||||
return fmt.Errorf("invalid tap action coordinates: %v", actionCoordinates)
|
||||
}
|
||||
imagePath = filepath.Join(
|
||||
config.GetConfig().ScreenShotsPath,
|
||||
fmt.Sprintf("%s_%s.png", timestamp, actionType),
|
||||
)
|
||||
x, y := actionCoordinates[0], actionCoordinates[1]
|
||||
point := image.Point{X: int(x), Y: int(y)}
|
||||
err = SaveImageWithCircleMarker(compressedBufSource, point, imagePath)
|
||||
@@ -332,10 +332,6 @@ func MarkUIOperation(driver IDriver, actionType ActionMethod, actionCoordinates
|
||||
if len(actionCoordinates) != 4 {
|
||||
return fmt.Errorf("invalid swipe action coordinates: %v", actionCoordinates)
|
||||
}
|
||||
imagePath = filepath.Join(
|
||||
config.GetConfig().ScreenShotsPath,
|
||||
fmt.Sprintf("%s_%s.png", timestamp, actionType),
|
||||
)
|
||||
fromX, fromY := actionCoordinates[0], actionCoordinates[1]
|
||||
toX, toY := actionCoordinates[2], actionCoordinates[3]
|
||||
from := image.Point{X: int(fromX), Y: int(fromY)}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (dExt *XTDriver) TapByOCR(text string, opts ...option.ActionOption) error {
|
||||
point = textRect.Center()
|
||||
}
|
||||
log.Info().Str("text", text).Interface("rawTextRect", textRect).
|
||||
Interface("tapPoint", point).Msg("TapByOCR success")
|
||||
Interface("tapPoint", point).Msg("TapByOCR")
|
||||
|
||||
return dExt.TapAbsXY(point.X, point.Y, opts...)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (dExt *XTDriver) TapByCV(opts ...option.ActionOption) error {
|
||||
point = uiResult.Center()
|
||||
}
|
||||
log.Info().Interface("rawUIResult", uiResult).
|
||||
Interface("tapPoint", point).Msg("TapByCV success")
|
||||
Interface("tapPoint", point).Msg("TapByCV")
|
||||
|
||||
return dExt.TapAbsXY(point.X, point.Y, opts...)
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ func TestSaveImageWithArrow(t *testing.T) {
|
||||
func TestMarkOperation(t *testing.T) {
|
||||
driver := setupDriverExt(t)
|
||||
|
||||
opts := []option.ActionOption{option.WithMarkOperationEnabled(true)}
|
||||
opts := []option.ActionOption{option.WithPreMarkOperation(true)}
|
||||
|
||||
// tap point
|
||||
err := driver.TapXY(0.5, 0.5, opts...)
|
||||
|
||||
@@ -1,18 +1,56 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func handlerTapAbsXY(driver IDriver, rawX, rawY float64, opts ...option.ActionOption) (
|
||||
// Call custom function, used for pre/post action hook
|
||||
func (dExt *XTDriver) Call(desc string, fn func(), opts ...option.ActionOption) error {
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
log.Info().Str("desc", desc).
|
||||
Int64("duration(ms)", time.Since(startTime).Milliseconds()).
|
||||
Msg("function called")
|
||||
}()
|
||||
|
||||
if actionOptions.Timeout == 0 {
|
||||
// wait for function to finish
|
||||
fn()
|
||||
return nil
|
||||
}
|
||||
|
||||
// set timeout for function execution
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
fn()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// function completed within timeout
|
||||
return nil
|
||||
case <-time.After(time.Duration(actionOptions.Timeout) * time.Second):
|
||||
return fmt.Errorf("function execution exceeded timeout of %d seconds", actionOptions.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
||||
x, y float64, err error) {
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y = actionOptions.ApplyTapOffset(rawX, rawY)
|
||||
x, y = options.ApplyTapOffset(rawX, rawY)
|
||||
|
||||
// mark UI operation
|
||||
if actionOptions.MarkOperationEnabled {
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_TapAbsXY, []float64{x, y}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark tap operation")
|
||||
}
|
||||
@@ -21,7 +59,7 @@ func handlerTapAbsXY(driver IDriver, rawX, rawY float64, opts ...option.ActionOp
|
||||
return x, y, nil
|
||||
}
|
||||
|
||||
func handlerDoubleTap(driver IDriver, rawX, rawY float64, opts ...option.ActionOption) (
|
||||
func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
||||
x, y float64, err error) {
|
||||
|
||||
x, y, err = convertToAbsolutePoint(driver, rawX, rawY)
|
||||
@@ -29,11 +67,10 @@ func handlerDoubleTap(driver IDriver, rawX, rawY float64, opts ...option.ActionO
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y = actionOptions.ApplyTapOffset(x, y)
|
||||
x, y = options.ApplyTapOffset(x, y)
|
||||
|
||||
// mark UI operation
|
||||
if actionOptions.MarkOperationEnabled {
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_DoubleTapXY, []float64{x, y}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark double tap operation")
|
||||
}
|
||||
@@ -42,18 +79,17 @@ func handlerDoubleTap(driver IDriver, rawX, rawY float64, opts ...option.ActionO
|
||||
return x, y, nil
|
||||
}
|
||||
|
||||
func handlerDrag(driver IDriver, rawFomX, rawFromY, rawToX, rawToY float64, opts ...option.ActionOption) (
|
||||
func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
||||
fromX, fromY, toX, toY float64, err error) {
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
fromX, fromY, toX, toY = actionOptions.ApplySwipeOffset(fromX, fromY, toX, toY)
|
||||
fromX, fromY, toX, toY = options.ApplySwipeOffset(fromX, fromY, toX, toY)
|
||||
|
||||
// mark UI operation
|
||||
if actionOptions.MarkOperationEnabled {
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_Drag, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark drag operation")
|
||||
}
|
||||
@@ -62,18 +98,17 @@ func handlerDrag(driver IDriver, rawFomX, rawFromY, rawToX, rawToY float64, opts
|
||||
return fromX, fromY, toX, toY, nil
|
||||
}
|
||||
|
||||
func handlerSwipe(driver IDriver, rawFomX, rawFromY, rawToX, rawToY float64, opts ...option.ActionOption) (
|
||||
func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
||||
fromX, fromY, toX, toY float64, err error) {
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
fromX, fromY, toX, toY = actionOptions.ApplySwipeOffset(fromX, fromY, toX, toY)
|
||||
fromX, fromY, toX, toY = options.ApplySwipeOffset(fromX, fromY, toX, toY)
|
||||
|
||||
// mark UI operation
|
||||
if actionOptions.MarkOperationEnabled {
|
||||
// save screenshot before action and mark UI operation
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_Swipe, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark swipe operation")
|
||||
}
|
||||
@@ -81,3 +116,29 @@ func handlerSwipe(driver IDriver, rawFomX, rawFromY, rawToX, rawToY float64, opt
|
||||
|
||||
return fromX, fromY, toX, toY, nil
|
||||
}
|
||||
|
||||
func postHandler(driver IDriver, actionType ActionMethod, options *option.ActionOptions) error {
|
||||
// save screenshot after action
|
||||
if options.PostMarkOperation {
|
||||
// get compressed screenshot buffer
|
||||
compressBufSource, err := getScreenShotBuffer(driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save compressed screenshot to file
|
||||
timestamp := builtin.GenNameWithTimestamp("%d")
|
||||
imagePath := filepath.Join(
|
||||
config.GetConfig().ScreenShotsPath,
|
||||
fmt.Sprintf("action_%s_post_%s.png", timestamp, actionType),
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := saveScreenShot(compressBufSource, imagePath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("save screenshot file failed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -154,14 +154,13 @@ func (hd *HDCDriver) TapXY(x, y float64, opts ...option.ActionOption) error {
|
||||
|
||||
func (hd *HDCDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("x", x).Float64("y", y).Msg("HDCDriver.TapAbsXY")
|
||||
|
||||
var err error
|
||||
x, y, err = handlerTapAbsXY(hd, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_TapAbsXY(hd, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(hd, ACTION_TapAbsXY, actionOptions)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
if actionOptions.Identifier != "" {
|
||||
startTime := int(time.Now().UnixMilli())
|
||||
hd.points = append(hd.points, ExportPoint{Start: startTime, End: startTime + 100, Ext: actionOptions.Identifier, RunTime: 100})
|
||||
@@ -186,12 +185,14 @@ func (hd *HDCDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO
|
||||
func (hd *HDCDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
|
||||
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
|
||||
Float64("toX", toX).Float64("toY", toY).Msg("HDCDriver.Swipe")
|
||||
var err error
|
||||
fromX, fromY, toX, toY, err = handlerSwipe(hd, fromX, fromY, toX, toY)
|
||||
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err := preHandler_Swipe(hd, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
defer postHandler(hd, ACTION_Swipe, actionOptions)
|
||||
|
||||
duration := 200
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = int(actionOptions.PressDuration * 1000)
|
||||
|
||||
@@ -597,11 +597,12 @@ func (wd *WDADriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
x = wd.toScale(x)
|
||||
y = wd.toScale(y)
|
||||
|
||||
var err error
|
||||
x, y, err = handlerTapAbsXY(wd, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_TapAbsXY(wd, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_TapAbsXY, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
@@ -621,11 +622,12 @@ func (wd *WDADriver) DoubleTap(x, y float64, opts ...option.ActionOption) error
|
||||
x = wd.toScale(x)
|
||||
y = wd.toScale(y)
|
||||
|
||||
var err error
|
||||
x, y, err = handlerDoubleTap(wd, x, y, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
x, y, err := preHandler_DoubleTap(wd, actionOptions, x, y)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_DoubleTapXY, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
@@ -657,11 +659,12 @@ func (wd *WDADriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO
|
||||
toX = wd.toScale(toX)
|
||||
toY = wd.toScale(toY)
|
||||
|
||||
var err error
|
||||
fromX, fromY, toX, toY, err = handlerDrag(wd, fromX, fromY, toX, toY, opts...)
|
||||
actionOptions := option.NewActionOptions(opts...)
|
||||
fromX, fromY, toX, toY, err := preHandler_Drag(wd, actionOptions, fromX, fromY, toX, toY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_Drag, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"fromX": math.Round(fromX*10) / 10,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math/rand/v2"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ActionOptions struct {
|
||||
@@ -72,10 +73,22 @@ func (o *ActionOptions) Options() []ActionOption {
|
||||
case []interface{}:
|
||||
// loaded from json case
|
||||
// custom direction: [fromX, fromY, toX, toY]
|
||||
sx, _ := builtin.Interface2Float64(v[0])
|
||||
sy, _ := builtin.Interface2Float64(v[1])
|
||||
ex, _ := builtin.Interface2Float64(v[2])
|
||||
ey, _ := builtin.Interface2Float64(v[3])
|
||||
sx, err := builtin.Interface2Float64(v[0])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Interface("fromX", v[0]).Msg("convert float64 failed")
|
||||
}
|
||||
sy, err := builtin.Interface2Float64(v[1])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Interface("fromY", v[1]).Msg("convert float64 failed")
|
||||
}
|
||||
ex, err := builtin.Interface2Float64(v[2])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Interface("toX", v[2]).Msg("convert float64 failed")
|
||||
}
|
||||
ey, err := builtin.Interface2Float64(v[3])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Interface("toY", v[3]).Msg("convert float64 failed")
|
||||
}
|
||||
options = append(options, WithCustomDirection(
|
||||
sx, sy,
|
||||
ex, ey,
|
||||
|
||||
@@ -277,8 +277,8 @@ func WithIndex(index int) ActionOption {
|
||||
|
||||
// MarkOperationOptions contains options for marking UI operations
|
||||
type MarkOperationOptions struct {
|
||||
// mark UI operation, enable/disable UI operation marking
|
||||
MarkOperationEnabled bool `json:"mark_operation_enabled,omitempty" yaml:"mark_operation_enabled,omitempty"`
|
||||
PreMarkOperation bool `json:"pre_mark_operation,omitempty" yaml:"pre_mark_operation,omitempty"`
|
||||
PostMarkOperation bool `json:"post_mark_operation,omitempty" yaml:"post_mark_operation,omitempty"`
|
||||
}
|
||||
|
||||
func (o *MarkOperationOptions) GetMarkOperationOptions() []ActionOption {
|
||||
@@ -287,16 +287,26 @@ func (o *MarkOperationOptions) GetMarkOperationOptions() []ActionOption {
|
||||
return options
|
||||
}
|
||||
|
||||
if o.MarkOperationEnabled {
|
||||
options = append(options, WithMarkOperationEnabled(true))
|
||||
if o.PreMarkOperation {
|
||||
options = append(options, WithPreMarkOperation(true))
|
||||
}
|
||||
if o.PostMarkOperation {
|
||||
options = append(options, WithPostMarkOperation(true))
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithMarkOperationEnabled enables or disables UI operation marking
|
||||
func WithMarkOperationEnabled(enabled bool) ActionOption {
|
||||
// WithPreMarkOperation enables UI operation marking before action
|
||||
func WithPreMarkOperation(enabled bool) ActionOption {
|
||||
return func(o *ActionOptions) {
|
||||
o.MarkOperationEnabled = enabled
|
||||
o.PreMarkOperation = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// WithPostMarkOperation enables UI operation marking after action
|
||||
func WithPostMarkOperation(enabled bool) ActionOption {
|
||||
return func(o *ActionOptions) {
|
||||
o.PostMarkOperation = enabled
|
||||
}
|
||||
}
|
||||
|
||||
107
uixt/types/field.go
Normal file
107
uixt/types/field.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// IntOrString supports int or string
|
||||
type IntOrString struct {
|
||||
IntValue *int // e.g 513
|
||||
StringValue *string // e.g "513", "$var"
|
||||
}
|
||||
|
||||
// Value returns the int value, converting from string if necessary
|
||||
func (ios *IntOrString) Value() (int, error) {
|
||||
if ios == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if ios.IntValue != nil {
|
||||
return *ios.IntValue, nil
|
||||
}
|
||||
if ios.StringValue != nil {
|
||||
if *ios.StringValue == "" {
|
||||
return 0, nil
|
||||
}
|
||||
n, err := strconv.Atoi(*ios.StringValue)
|
||||
if err != nil {
|
||||
// variable expression, e.g. "$var"
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// IntValue and StringValue are both nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshalling for IntOrString
|
||||
func (ios *IntOrString) UnmarshalJSON(data []byte) error {
|
||||
// Try to unmarshal as int
|
||||
var i int
|
||||
if err := json.Unmarshal(data, &i); err == nil {
|
||||
ios.IntValue = &i
|
||||
ios.StringValue = nil
|
||||
return nil
|
||||
}
|
||||
// Try to unmarshal as string
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
ios.StringValue = &s
|
||||
ios.IntValue = nil
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid IntOrString data: %s", string(data))
|
||||
}
|
||||
|
||||
// FloatOrString supports float64 or string
|
||||
type FloatOrString struct {
|
||||
FloatValue *float64 // e.g 5.13
|
||||
StringValue *string // e.g "5.13", "$var"
|
||||
}
|
||||
|
||||
// Value returns the float value, converting from string if necessary
|
||||
func (ios *FloatOrString) Value() (float64, error) {
|
||||
if ios == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if ios.FloatValue != nil {
|
||||
return *ios.FloatValue, nil
|
||||
}
|
||||
if ios.StringValue != nil {
|
||||
if *ios.StringValue == "" {
|
||||
return 0, nil
|
||||
}
|
||||
n, err := strconv.ParseFloat(*ios.StringValue, 64)
|
||||
if err != nil {
|
||||
// variable expression, e.g. "$var"
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// IntValue and StringValue are both nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshalling for IntOrString
|
||||
func (ios *FloatOrString) UnmarshalJSON(data []byte) error {
|
||||
// Try to unmarshal as float
|
||||
var f float64
|
||||
if err := json.Unmarshal(data, &f); err == nil {
|
||||
ios.FloatValue = &f
|
||||
ios.StringValue = nil
|
||||
return nil
|
||||
}
|
||||
// Try to unmarshal as string
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
ios.StringValue = &s
|
||||
ios.FloatValue = nil
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid FloatOrString data: %s", string(data))
|
||||
}
|
||||
Reference in New Issue
Block a user