From 3715cbb4325af697eb63aa9cf169812a070e5cb3 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 9 May 2025 23:01:27 +0800 Subject: [PATCH 01/19] feat: support pre hook and post hook for actions --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 5 +++-- uixt/android_driver_uia2.go | 7 +++---- uixt/android_test.go | 28 ++++++++++++++++++++++++++++ uixt/browser_driver.go | 6 +++--- uixt/driver_handler.go | 17 +++++++++++++---- uixt/harmony_driver_hdc.go | 7 +++---- uixt/ios_driver_wda.go | 5 +++-- uixt/option/action.go | 1 + uixt/option/hook.go | 31 +++++++++++++++++++++++++++++++ 10 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 uixt/option/hook.go diff --git a/internal/version/VERSION b/internal/version/VERSION index 3c87b0d6..1c2ecd0c 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505091122 +v5.0.0-beta-2505092301 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index e7711ecf..d0d129f8 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -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, actionOptions) // adb shell input tap x y xStr := fmt.Sprintf("%.1f", x) diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 0055f764..0aca302a 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -298,14 +298,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, actionOptions) - actionOptions := option.NewActionOptions(opts...) duration := 100.0 if actionOptions.PressDuration > 0 { duration = actionOptions.PressDuration * 1000 // convert to ms diff --git a/uixt/android_test.go b/uixt/android_test.go index be2dac5b..c5930693 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -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,33 @@ func TestDriver_ADB_TapXY(t *testing.T) { assert.Nil(t, err) } +func TestDriver_ADB_TapXY_WithHook(t *testing.T) { + driver := setupADBDriverExt(t) + x, y := 0.4, 0.5 + err := driver.TapXY(x, y, + option.WithHooks( + func() { + log.Info().Msg("pre hook") + x += 1 + }, + func() { + log.Info().Msg("post hook") + }, + ), + ) + assert.Nil(t, err) + + err = driver.TapXY(0.4, 0.5, + option.WithPreHook(func() { + log.Info().Msg("pre hook") + }), + option.WithPostHook(func() { + log.Info().Msg("post hook") + }), + ) + assert.Nil(t, err) +} + func TestDriver_ADB_TapAbsXY(t *testing.T) { driver := setupADBDriverExt(t) err := driver.TapAbsXY(100, 300) diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 35caa7b0..429a3bca 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -511,13 +511,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, actionOptions) - actionOptions := option.NewActionOptions(opts...) duration := 0.1 if actionOptions.Duration > 0 { duration = actionOptions.Duration diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index f237d0e8..4e6452d1 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -5,14 +5,17 @@ import ( "github.com/rs/zerolog/log" ) -func handlerTapAbsXY(driver IDriver, rawX, rawY float64, opts ...option.ActionOption) ( +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) + if options.PreHook != nil { + options.PreHook() + } + + x, y = options.ApplyTapOffset(rawX, rawY) // mark UI operation - if actionOptions.MarkOperationEnabled { + if options.MarkOperationEnabled { if markErr := MarkUIOperation(driver, ACTION_TapAbsXY, []float64{x, y}); markErr != nil { log.Warn().Err(markErr).Msg("Failed to mark tap operation") } @@ -81,3 +84,9 @@ func handlerSwipe(driver IDriver, rawFomX, rawFromY, rawToX, rawToY float64, opt return fromX, fromY, toX, toY, nil } + +func postHandler(_ IDriver, options *option.ActionOptions) { + if options.PostHook != nil { + options.PostHook() + } +} diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index 0865e365..a160b124 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -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, 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}) diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 889c95c0..a2040c0e 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -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, actionOptions) data := map[string]interface{}{ "x": x, diff --git a/uixt/option/action.go b/uixt/option/action.go index 81911ec8..b8ad41a1 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -23,6 +23,7 @@ type ActionOptions struct { Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"` ScreenOptions + HookOptions // set custiom options such as textview, id, description Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"` diff --git a/uixt/option/hook.go b/uixt/option/hook.go new file mode 100644 index 00000000..54fbb55a --- /dev/null +++ b/uixt/option/hook.go @@ -0,0 +1,31 @@ +package option + +// HookOptions contains options for action hooks +type HookOptions struct { + // pre hook before action + PreHook func() + // post hook after action + PostHook func() +} + +// WithPreHook sets the pre hook before action +func WithPreHook(preHook func()) ActionOption { + return func(o *ActionOptions) { + o.PreHook = preHook + } +} + +// WithPostHook sets the post hook after action +func WithPostHook(postHook func()) ActionOption { + return func(o *ActionOptions) { + o.PostHook = postHook + } +} + +// WithHooks sets the pre hook and post hook +func WithHooks(preHook func(), postHook func()) ActionOption { + return func(o *ActionOptions) { + o.PreHook = preHook + o.PostHook = postHook + } +} From f7ec4a06b44e3426e900c2314a4d4ee8babdf0e8 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 9 May 2025 23:06:45 +0800 Subject: [PATCH 02/19] feat: add pre hook and post hook for DoubleTap action --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 5 +++-- uixt/android_driver_uia2.go | 5 +++-- uixt/browser_driver.go | 6 ++++-- uixt/driver_handler.go | 11 +++++++---- uixt/ios_driver_wda.go | 5 +++-- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 1c2ecd0c..4c099004 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505092301 +v5.0.0-beta-2505092306 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index d0d129f8..6ecd6b6d 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -326,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, actionOptions) // adb shell input tap x y xStr := fmt.Sprintf("%.1f", x) diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 0aca302a..9252904f 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -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, actionOptions) data := map[string]interface{}{ "actions": []interface{}{ diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 429a3bca..dcab3412 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -535,11 +535,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, actionOptions) + data := map[string]interface{}{ "x": x, "y": y, diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 4e6452d1..b7789ec9 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -24,19 +24,22 @@ func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, ra 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) { + if options.PreHook != nil { + options.PreHook() + } + x, y, err = convertToAbsolutePoint(driver, rawX, rawY) if err != nil { 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.MarkOperationEnabled { if markErr := MarkUIOperation(driver, ACTION_DoubleTapXY, []float64{x, y}); markErr != nil { log.Warn().Err(markErr).Msg("Failed to mark double tap operation") } diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index a2040c0e..8f6b57d2 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -622,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, actionOptions) data := map[string]interface{}{ "x": x, From 1dfc473d3316d8da5b7544c060edb7ec99d477a4 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 9 May 2025 23:10:59 +0800 Subject: [PATCH 03/19] feat: add pre hook and post hook for Drag action --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 5 +++-- uixt/android_driver_uia2.go | 5 +++-- uixt/browser_driver.go | 6 ++++-- uixt/driver_handler.go | 11 +++++++---- uixt/ios_driver_wda.go | 5 +++-- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 4c099004..f53e0f57 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505092306 +v5.0.0-beta-2505092310 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 6ecd6b6d..b8248d32 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -375,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, actionOptions) - actionOptions := option.NewActionOptions(opts...) duration := 200.0 if actionOptions.Duration > 0 { duration = actionOptions.Duration * 1000 diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 9252904f..124592fc 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -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, actionOptions) data := map[string]interface{}{ "startX": fromX, diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index dcab3412..044f3fb9 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -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, 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 diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index b7789ec9..36479885 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -48,18 +48,21 @@ func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, r 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...) + if options.PreHook != nil { + options.PreHook() + } + 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.MarkOperationEnabled { if markErr := MarkUIOperation(driver, ACTION_Drag, []float64{fromX, fromY, toX, toY}); markErr != nil { log.Warn().Err(markErr).Msg("Failed to mark drag operation") } diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 8f6b57d2..2fc3b4f8 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -659,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, actionOptions) data := map[string]interface{}{ "fromX": math.Round(fromX*10) / 10, From 2a13594e3d79309e7e2cba7c1b0a9b77f0f7527c Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 9 May 2025 23:15:27 +0800 Subject: [PATCH 04/19] feat: add pre hook and post hook for Swipe action --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 6 ++++-- uixt/android_driver_uia2.go | 8 +++++--- uixt/driver_handler.go | 11 +++++++---- uixt/harmony_driver_hdc.go | 8 +++++--- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index f53e0f57..335f1f9c 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505092310 +v5.0.0-beta-2505092315 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index b8248d32..0fa08110 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -406,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, actionOptions) // adb shell input swipe fromX fromY toX toY _, err = ad.runShellCommand( diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 124592fc..3b961a0f 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -392,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, actionOptions) + duration := 200.0 if actionOptions.PressDuration > 0 { duration = actionOptions.PressDuration * 1000 // ms diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 36479885..ec81fe29 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -71,18 +71,21 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw 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...) + if options.PreHook != nil { + options.PreHook() + } + 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.MarkOperationEnabled { if markErr := MarkUIOperation(driver, ACTION_Swipe, []float64{fromX, fromY, toX, toY}); markErr != nil { log.Warn().Err(markErr).Msg("Failed to mark swipe operation") } diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index a160b124..c12b831d 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -185,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, actionOptions) + duration := 200 if actionOptions.PressDuration > 0 { duration = int(actionOptions.PressDuration * 1000) From 4a2276c7f0a630d605a3dfd4e7ddd875e5c5164a Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 9 May 2025 23:30:35 +0800 Subject: [PATCH 05/19] refactor: 1, add options for AppLaunch/AppTerminate/AppClear; 2, add pre hook and post hook for AppLaunch/AppTerminate action --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 16 +++++++++++++--- uixt/browser_driver.go | 14 +++++++++++--- uixt/driver.go | 6 +++--- uixt/driver_handler.go | 16 ++++++++++++++++ uixt/harmony_driver_hdc.go | 15 ++++++++++++--- uixt/ios_driver_wda.go | 16 +++++++++++++--- 7 files changed, 69 insertions(+), 16 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 335f1f9c..6a4c76d1 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505092315 +v5.0.0-beta-2505092330 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 0fa08110..80493956 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -266,10 +266,15 @@ func (ad *ADBDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error return } -func (ad *ADBDriver) AppLaunch(packageName string) (err error) { +func (ad *ADBDriver) AppLaunch(packageName string, opts ...option.ActionOption) (err error) { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppLaunch") // 不指定 Activity 名称启动(启动主 Activity) // adb shell monkey -p -c android.intent.category.LAUNCHER 1 + + actionOptions := option.NewActionOptions(opts...) + preHandler_AppLaunch(ad, actionOptions) + defer postHandler(ad, actionOptions) + sOutput, err := ad.runShellCommand( "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1", ) @@ -284,10 +289,15 @@ func (ad *ADBDriver) AppLaunch(packageName string) (err error) { return nil } -func (ad *ADBDriver) AppTerminate(packageName string) (successful bool, err error) { +func (ad *ADBDriver) AppTerminate(packageName string, opts ...option.ActionOption) (successful bool, err error) { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppTerminate") // 强制停止应用,停止 相关的进程 // adb shell am force-stop + + actionOptions := option.NewActionOptions(opts...) + preHandler_AppTerminate(ad, actionOptions) + defer postHandler(ad, actionOptions) + _, err = ad.runShellCommand("am", "force-stop", packageName) if err != nil { return false, errors.Wrap(err, "force-stop app failed") @@ -541,7 +551,7 @@ func (ad *ADBDriver) SendKeysByAdbKeyBoard(text string) (err error) { return } -func (ad *ADBDriver) AppClear(packageName string) error { +func (ad *ADBDriver) AppClear(packageName string, opts ...option.ActionOption) error { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppClear") if _, err := ad.runShellCommand("pm", "clear", packageName); err != nil { log.Error().Str("packageName", packageName).Err(err).Msg("failed to clear package cache") diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 044f3fb9..81933236 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -138,7 +138,11 @@ func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option. return } -func (wd *BrowserDriver) AppLaunch(packageName string) (err error) { +func (wd *BrowserDriver) AppLaunch(packageName string, opts ...option.ActionOption) (err error) { + actionOptions := option.NewActionOptions(opts...) + preHandler_AppLaunch(wd, actionOptions) + defer postHandler(wd, actionOptions) + data := map[string]interface{}{ "url": packageName, } @@ -461,7 +465,11 @@ func (wd *BrowserDriver) Unlock() (err error) { // AppTerminate Terminate an application with the given package name. // Either `true` if the app has been successfully terminated or `false` if it was not running -func (wd *BrowserDriver) AppTerminate(packageName string) (bool, error) { +func (wd *BrowserDriver) AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) { + actionOptions := option.NewActionOptions(opts...) + preHandler_AppTerminate(wd, actionOptions) + defer postHandler(wd, actionOptions) + return true, wd.DeleteSession() } @@ -474,7 +482,7 @@ func (wd *BrowserDriver) Back() error { return wd.PressBack() } -func (wd *BrowserDriver) AppClear(packageName string) error { +func (wd *BrowserDriver) AppClear(packageName string, opts ...option.ActionOption) error { return errors.New("not support") } diff --git a/uixt/driver.go b/uixt/driver.go index 72d5ab06..5a6e7420 100644 --- a/uixt/driver.go +++ b/uixt/driver.go @@ -69,9 +69,9 @@ type IDriver interface { Backspace(count int, opts ...option.ActionOption) error // app related - AppLaunch(packageName string) error - AppTerminate(packageName string) (bool, error) - AppClear(packageName string) error + AppLaunch(packageName string, opts ...option.ActionOption) error + AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) + AppClear(packageName string, opts ...option.ActionOption) error // image related PushImage(localPath string) error diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index ec81fe29..5b58f78d 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -94,6 +94,22 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra return fromX, fromY, toX, toY, nil } +func preHandler_AppLaunch(_ IDriver, options *option.ActionOptions) (err error) { + if options.PreHook != nil { + options.PreHook() + } + + return nil +} + +func preHandler_AppTerminate(_ IDriver, options *option.ActionOptions) (err error) { + if options.PreHook != nil { + options.PreHook() + } + + return nil +} + func postHandler(_ IDriver, options *option.ActionOptions) { if options.PostHook != nil { options.PostHook() diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index c12b831d..0719d33f 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -119,13 +119,22 @@ func (hd *HDCDriver) Unlock() (err error) { return hd.Swipe(500, 1500, 500, 500) } -func (hd *HDCDriver) AppLaunch(packageName string) error { +func (hd *HDCDriver) AppLaunch(packageName string, opts ...option.ActionOption) error { + actionOptions := option.NewActionOptions(opts...) + preHandler_AppLaunch(hd, actionOptions) + defer postHandler(hd, actionOptions) + // Todo return types.ErrDriverNotImplemented } -func (hd *HDCDriver) AppTerminate(packageName string) (bool, error) { +func (hd *HDCDriver) AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) { log.Info().Str("packageName", packageName).Msg("HDCDriver.AppTerminate") + + actionOptions := option.NewActionOptions(opts...) + preHandler_AppTerminate(hd, actionOptions) + defer postHandler(hd, actionOptions) + _, err := hd.Device.RunShellCommand("aa", "force-stop", packageName) if err != nil { log.Error().Err(err).Msg("failed to terminal app") @@ -214,7 +223,7 @@ func (hd *HDCDriver) Input(text string, opts ...option.ActionOption) error { return hd.uiDriver.InputText(text) } -func (hd *HDCDriver) AppClear(packageName string) error { +func (hd *HDCDriver) AppClear(packageName string, opts ...option.ActionOption) error { return types.ErrDriverNotImplemented } diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 2fc3b4f8..300c405d 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -491,9 +491,14 @@ func (wd *WDADriver) AlertSendKeys(text string) (err error) { return } -func (wd *WDADriver) AppLaunch(bundleId string) (err error) { +func (wd *WDADriver) AppLaunch(bundleId string, opts ...option.ActionOption) (err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppLaunch") // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] + + actionOptions := option.NewActionOptions(opts...) + preHandler_AppLaunch(wd, actionOptions) + defer postHandler(wd, actionOptions) + data := make(map[string]interface{}) data["bundleId"] = bundleId data["environment"] = map[string]interface{}{ @@ -520,9 +525,14 @@ func (wd *WDADriver) AppLaunchUnattached(bundleId string) (err error) { return nil } -func (wd *WDADriver) AppTerminate(bundleId string) (successful bool, err error) { +func (wd *WDADriver) AppTerminate(bundleId string, opts ...option.ActionOption) (successful bool, err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppTerminate") // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] + + actionOptions := option.NewActionOptions(opts...) + preHandler_AppTerminate(wd, actionOptions) + defer postHandler(wd, actionOptions) + data := map[string]interface{}{"bundleId": bundleId} var rawResp DriverRawResponse urlStr := fmt.Sprintf("/session/%s/wda/apps/terminate", wd.Session.ID) @@ -734,7 +744,7 @@ func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err erro return } -func (wd *WDADriver) AppClear(packageName string) error { +func (wd *WDADriver) AppClear(packageName string, opts ...option.ActionOption) error { return types.ErrDriverNotImplemented } From 9bafea53af74efe6d8acf78a6e6323dc7a558e62 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 10 May 2025 00:01:30 +0800 Subject: [PATCH 06/19] feat: support action options for AppLaunch/AppTerminate --- internal/version/VERSION | 2 +- step_ui.go | 14 ++++++++------ uixt/driver_action.go | 6 +++--- uixt/option/action.go | 1 + uixt/option/hook.go | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 6a4c76d1..174a2016 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505092330 +v5.0.0-beta-2505100001 diff --git a/step_ui.go b/step_ui.go index 185bbb59..3a723c87 100644 --- a/step_ui.go +++ b/step_ui.go @@ -91,18 +91,20 @@ func (s *StepMobile) WebLoginNoneUI(packageName, phoneNumber string, captcha, pa return s } -func (s *StepMobile) AppLaunch(bundleId string) *StepMobile { +func (s *StepMobile) AppLaunch(bundleId string, opts ...option.ActionOption) *StepMobile { s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{ - Method: uixt.ACTION_AppLaunch, - Params: bundleId, + Method: uixt.ACTION_AppLaunch, + Params: bundleId, + Options: option.NewActionOptions(opts...), }) return s } -func (s *StepMobile) AppTerminate(bundleId string) *StepMobile { +func (s *StepMobile) AppTerminate(bundleId string, opts ...option.ActionOption) *StepMobile { s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{ - Method: uixt.ACTION_AppTerminate, - Params: bundleId, + Method: uixt.ACTION_AppTerminate, + Params: bundleId, + Options: option.NewActionOptions(opts...), }) return s } diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 2a28a5d0..419258da 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -145,13 +145,13 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { } case ACTION_AppClear: if packageName, ok := action.Params.(string); ok { - if err = dExt.AppClear(packageName); err != nil { + if err = dExt.AppClear(packageName, action.GetOptions()...); err != nil { return errors.Wrap(err, "failed to clear app") } } case ACTION_AppLaunch: if bundleId, ok := action.Params.(string); ok { - return dExt.AppLaunch(bundleId) + return dExt.AppLaunch(bundleId, action.GetOptions()...) } return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", ACTION_AppLaunch, action.Params) @@ -177,7 +177,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { return fmt.Errorf("invalid %s params: %v", ACTION_SwipeToTapTexts, action.Params) case ACTION_AppTerminate: if bundleId, ok := action.Params.(string); ok { - success, err := dExt.AppTerminate(bundleId) + success, err := dExt.AppTerminate(bundleId, action.GetOptions()...) if err != nil { return errors.Wrap(err, "failed to terminate app") } diff --git a/uixt/option/action.go b/uixt/option/action.go index b8ad41a1..b826c8f0 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -134,6 +134,7 @@ func (o *ActionOptions) Options() []ActionOption { options = append(options, o.GetScreenShotOptions()...) options = append(options, o.GetScreenRecordOptions()...) options = append(options, o.GetMarkOperationOptions()...) + options = append(options, o.GetHookOptions()...) return options } diff --git a/uixt/option/hook.go b/uixt/option/hook.go index 54fbb55a..365bed75 100644 --- a/uixt/option/hook.go +++ b/uixt/option/hook.go @@ -8,6 +8,22 @@ type HookOptions struct { PostHook func() } +func (o *HookOptions) GetHookOptions() []ActionOption { + options := make([]ActionOption, 0) + if o == nil { + return options + } + + if o.PreHook != nil { + options = append(options, WithPreHook(o.PreHook)) + } + if o.PostHook != nil { + options = append(options, WithPostHook(o.PostHook)) + } + + return options +} + // WithPreHook sets the pre hook before action func WithPreHook(preHook func()) ActionOption { return func(o *ActionOptions) { From f6ad6c9effaab36daf460e0a0a3606996996ac2b Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 10 May 2025 09:50:00 +0800 Subject: [PATCH 07/19] feat: add function call for XTDriver --- internal/version/VERSION | 2 +- uixt/driver_action.go | 7 ++++--- uixt/driver_ext_tap.go | 4 ++-- uixt/driver_handler.go | 13 +++++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 174a2016..aefbbf5f 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505100001 +v5.0.0-beta-2505100950 diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 419258da..0f79c981 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -335,9 +335,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) + } + 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()...) diff --git a/uixt/driver_ext_tap.go b/uixt/driver_ext_tap.go index 2458a3a8..dcb08753 100644 --- a/uixt/driver_ext_tap.go +++ b/uixt/driver_ext_tap.go @@ -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...) } diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 5b58f78d..2fcc321d 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -1,10 +1,23 @@ package uixt import ( + "time" + "github.com/httprunner/httprunner/v5/uixt/option" "github.com/rs/zerolog/log" ) +// Call custom function, used for pre/post hook for actions +func (dExt *XTDriver) Call(desc string, fn func()) error { + startTime := time.Now() + fn() + log.Info().Str("desc", desc). + Int64("duration(ms)", time.Since(startTime).Milliseconds()). + Msg("function called") + + return nil +} + func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, rawY float64) ( x, y float64, err error) { From 7a6890a160583420d1e4fa1c5e2e3f10c648a703 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 08:47:47 +0800 Subject: [PATCH 08/19] feat: set timeout for Call function --- internal/version/VERSION | 2 +- step_ui.go | 4 ++-- uixt/driver_action.go | 2 +- uixt/driver_handler.go | 39 +++++++++++++++++++++++++++++++-------- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index aefbbf5f..71fc4cdc 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505100950 +v5.0.0-beta-2505120848 diff --git a/step_ui.go b/step_ui.go index 3a723c87..1c3e5d38 100644 --- a/step_ui.go +++ b/step_ui.go @@ -449,12 +449,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 } diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 0f79c981..767e27e4 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -336,7 +336,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { return dExt.ClosePopupsHandler() case ACTION_CallFunction: if funcDesc, ok := action.Params.(string); ok { - return dExt.Call(funcDesc, action.Fn) + return dExt.Call(funcDesc, action.Fn, action.GetOptions()...) } return fmt.Errorf("invalid function description: %v", action.Params) case ACTION_AIAction: diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 2fcc321d..372bfca6 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -1,21 +1,44 @@ package uixt import ( + "fmt" "time" "github.com/httprunner/httprunner/v5/uixt/option" "github.com/rs/zerolog/log" ) -// Call custom function, used for pre/post hook for actions -func (dExt *XTDriver) Call(desc string, fn func()) error { - startTime := time.Now() - fn() - log.Info().Str("desc", desc). - Int64("duration(ms)", time.Since(startTime).Milliseconds()). - Msg("function called") +// 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...) - return nil + 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) ( From d95eec78b0a23d559e690522764220ff3565867b Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 08:58:27 +0800 Subject: [PATCH 09/19] feat: add WithPreMarkOperation and WithPostMarkOperation to mark UI operation before/after action --- internal/version/VERSION | 2 +- uixt/android_test.go | 30 +++++++++---------------- uixt/driver_ext_test.go | 2 +- uixt/driver_handler.go | 35 ++++-------------------------- uixt/option/action.go | 2 -- uixt/option/hook.go | 47 ---------------------------------------- uixt/option/screen.go | 24 ++++++++++++++------ 7 files changed, 33 insertions(+), 109 deletions(-) delete mode 100644 uixt/option/hook.go diff --git a/internal/version/VERSION b/internal/version/VERSION index 71fc4cdc..14b317de 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505120848 +v5.0.0-beta-2505120858 diff --git a/uixt/android_test.go b/uixt/android_test.go index c5930693..24eef80b 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -136,28 +136,18 @@ func TestDriver_ADB_TapXY(t *testing.T) { func TestDriver_ADB_TapXY_WithHook(t *testing.T) { driver := setupADBDriverExt(t) - x, y := 0.4, 0.5 - err := driver.TapXY(x, y, - option.WithHooks( - func() { - log.Info().Msg("pre hook") - x += 1 - }, - func() { - log.Info().Msg("post hook") - }, - ), - ) + + 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, - option.WithPreHook(func() { - log.Info().Msg("pre hook") - }), - option.WithPostHook(func() { - log.Info().Msg("post hook") - }), - ) + 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) } diff --git a/uixt/driver_ext_test.go b/uixt/driver_ext_test.go index 873cf52e..36250155 100644 --- a/uixt/driver_ext_test.go +++ b/uixt/driver_ext_test.go @@ -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...) diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 372bfca6..640b20eb 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -44,14 +44,10 @@ func (dExt *XTDriver) Call(desc string, fn func(), opts ...option.ActionOption) func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, rawY float64) ( x, y float64, err error) { - if options.PreHook != nil { - options.PreHook() - } - x, y = options.ApplyTapOffset(rawX, rawY) // mark UI operation - if options.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") } @@ -63,10 +59,6 @@ func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, ra func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, rawY float64) ( x, y float64, err error) { - if options.PreHook != nil { - options.PreHook() - } - x, y, err = convertToAbsolutePoint(driver, rawX, rawY) if err != nil { return 0, 0, err @@ -75,7 +67,7 @@ func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, r x, y = options.ApplyTapOffset(x, y) // mark UI operation - if options.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") } @@ -87,10 +79,6 @@ func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, r func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) ( fromX, fromY, toX, toY float64, err error) { - if options.PreHook != nil { - options.PreHook() - } - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY) if err != nil { return 0, 0, 0, 0, err @@ -98,7 +86,7 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw fromX, fromY, toX, toY = options.ApplySwipeOffset(fromX, fromY, toX, toY) // mark UI operation - if options.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") } @@ -110,10 +98,6 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) ( fromX, fromY, toX, toY float64, err error) { - if options.PreHook != nil { - options.PreHook() - } - fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY) if err != nil { return 0, 0, 0, 0, err @@ -121,7 +105,7 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra fromX, fromY, toX, toY = options.ApplySwipeOffset(fromX, fromY, toX, toY) // mark UI operation - if options.MarkOperationEnabled { + 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") } @@ -131,23 +115,12 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra } func preHandler_AppLaunch(_ IDriver, options *option.ActionOptions) (err error) { - if options.PreHook != nil { - options.PreHook() - } - return nil } func preHandler_AppTerminate(_ IDriver, options *option.ActionOptions) (err error) { - if options.PreHook != nil { - options.PreHook() - } - return nil } func postHandler(_ IDriver, options *option.ActionOptions) { - if options.PostHook != nil { - options.PostHook() - } } diff --git a/uixt/option/action.go b/uixt/option/action.go index b826c8f0..81911ec8 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -23,7 +23,6 @@ type ActionOptions struct { Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"` ScreenOptions - HookOptions // set custiom options such as textview, id, description Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"` @@ -134,7 +133,6 @@ func (o *ActionOptions) Options() []ActionOption { options = append(options, o.GetScreenShotOptions()...) options = append(options, o.GetScreenRecordOptions()...) options = append(options, o.GetMarkOperationOptions()...) - options = append(options, o.GetHookOptions()...) return options } diff --git a/uixt/option/hook.go b/uixt/option/hook.go deleted file mode 100644 index 365bed75..00000000 --- a/uixt/option/hook.go +++ /dev/null @@ -1,47 +0,0 @@ -package option - -// HookOptions contains options for action hooks -type HookOptions struct { - // pre hook before action - PreHook func() - // post hook after action - PostHook func() -} - -func (o *HookOptions) GetHookOptions() []ActionOption { - options := make([]ActionOption, 0) - if o == nil { - return options - } - - if o.PreHook != nil { - options = append(options, WithPreHook(o.PreHook)) - } - if o.PostHook != nil { - options = append(options, WithPostHook(o.PostHook)) - } - - return options -} - -// WithPreHook sets the pre hook before action -func WithPreHook(preHook func()) ActionOption { - return func(o *ActionOptions) { - o.PreHook = preHook - } -} - -// WithPostHook sets the post hook after action -func WithPostHook(postHook func()) ActionOption { - return func(o *ActionOptions) { - o.PostHook = postHook - } -} - -// WithHooks sets the pre hook and post hook -func WithHooks(preHook func(), postHook func()) ActionOption { - return func(o *ActionOptions) { - o.PreHook = preHook - o.PostHook = postHook - } -} diff --git a/uixt/option/screen.go b/uixt/option/screen.go index 70b4a1b3..6b90e951 100644 --- a/uixt/option/screen.go +++ b/uixt/option/screen.go @@ -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 } } From 4dfeffc32b8b931146656164ee24d79d5923c3ca Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 09:18:05 +0800 Subject: [PATCH 10/19] refactor: RegisterUIXTDrivers --- internal/version/VERSION | 2 +- runner.go | 106 ++++++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 14b317de..c3714433 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505120858 +v5.0.0-beta-2505120918 diff --git a/runner.go b/runner.go index 4344d4fa..7ea20de8 100644 --- a/runner.go +++ b/runner.go @@ -329,6 +329,27 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { return caseRunner, nil } +func NewCaseRunner(testcase TestCase) (*CaseRunner, error) { + caseRunner := &CaseRunner{ + TestCase: testcase, + } + // config := testcase.Config.Get() + + // TODO: init parser plugin + + // parse testcase config + parsedConfig, err := caseRunner.parseConfig() + if err != nil { + return nil, errors.Wrap(err, "parse testcase config failed") + } + + // TODO: set request timeout in seconds + // TODO: set testcase timeout in seconds + + caseRunner.TestCase.Config = parsedConfig + return caseRunner, nil +} + type CaseRunner struct { TestCase // each testcase init its own CaseRunner @@ -418,115 +439,128 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { } r.parametersIterator = parametersIterator + // register uixt drivers + if err := r.RegisterUIXTDrivers(parsedConfig); err != nil { + return nil, errors.Wrap(err, "register uixt drivers failed") + } + + return parsedConfig, nil +} + +func (r *CaseRunner) RegisterUIXTDrivers(config *TConfig) error { // ai options aiOpts := []option.AIServiceOption{} - if parsedConfig.LLMService != "" { - aiOpts = append(aiOpts, option.WithLLMService(option.LLMServiceType(parsedConfig.LLMService))) + if config.LLMService != "" { + aiOpts = append(aiOpts, option.WithLLMService(option.LLMServiceType(config.LLMService))) } - if parsedConfig.CVService == "" { + if config.CVService == "" { // default to vedem - parsedConfig.CVService = option.CVServiceTypeVEDEM + config.CVService = option.CVServiceTypeVEDEM } - aiOpts = append(aiOpts, option.WithCVService(parsedConfig.CVService)) + aiOpts = append(aiOpts, option.WithCVService(config.CVService)) // parse android devices config - for _, androidDeviceOptions := range parsedConfig.Android { - err := r.parseDeviceConfig(androidDeviceOptions, parsedConfig.Variables) + for _, androidDeviceOptions := range config.Android { + err := r.parseDeviceConfig(androidDeviceOptions, config.Variables) if err != nil { - return nil, errors.Wrap(code.InvalidCaseError, + return errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse android config failed: %v", err)) } device, err := uixt.NewAndroidDevice(androidDeviceOptions.Options()...) if err != nil { - return nil, errors.Wrap(err, "init android device failed") + return errors.Wrap(err, "init android device failed") } driver, err := device.NewDriver() if err != nil { - return nil, errors.Wrap(err, "init android driver failed") + return errors.Wrap(err, "init android driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return nil, errors.Wrap(err, "init android XTDriver failed") + return 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 { - err := r.parseDeviceConfig(iosDeviceOptions, parsedConfig.Variables) + for _, iosDeviceOptions := range config.IOS { + err := r.parseDeviceConfig(iosDeviceOptions, config.Variables) if err != nil { - return nil, errors.Wrap(code.InvalidCaseError, + return errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse ios config failed: %v", err)) } device, err := uixt.NewIOSDevice(iosDeviceOptions.Options()...) if err != nil { - return nil, errors.Wrap(err, "init ios device failed") + return errors.Wrap(err, "init ios device failed") } driver, err := device.NewDriver() if err != nil { - return nil, errors.Wrap(err, "init ios driver failed") + return errors.Wrap(err, "init ios driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return nil, errors.Wrap(err, "init ios XTDriver failed") + return 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 { - err := r.parseDeviceConfig(harmonyDeviceOptions, parsedConfig.Variables) + for _, harmonyDeviceOptions := range config.Harmony { + err := r.parseDeviceConfig(harmonyDeviceOptions, config.Variables) if err != nil { - return nil, errors.Wrap(code.InvalidCaseError, + return errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse harmony config failed: %v", err)) } device, err := uixt.NewHarmonyDevice(harmonyDeviceOptions.Options()...) if err != nil { - return nil, errors.Wrap(err, "init harmony device failed") + return errors.Wrap(err, "init harmony device failed") } driver, err := device.NewDriver() if err != nil { - return nil, errors.Wrap(err, "init harmony driver failed") + return errors.Wrap(err, "init harmony driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return nil, errors.Wrap(err, "init harmony XTDriver failed") + return 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 { - err := r.parseDeviceConfig(browserDeviceOptions, parsedConfig.Variables) + for _, browserDeviceOptions := range config.Browser { + err := r.parseDeviceConfig(browserDeviceOptions, config.Variables) if err != nil { - return nil, errors.Wrap(code.InvalidCaseError, + return errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse browser config failed: %v", err)) } device, err := uixt.NewBrowserDevice(browserDeviceOptions.Options()...) if err != nil { - return nil, errors.Wrap(err, "init browser device failed") + return errors.Wrap(err, "init browser device failed") } if err := device.Setup(); err != nil { - return nil, err + return err } driver, err := device.NewDriver() if err != nil { - return nil, err + return errors.Wrap(err, "init browser driver failed") } if err := driver.Setup(); err != nil { - return nil, err + return 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") + return errors.Wrap(err, "init browser XTDriver failed") } - r.uixtDrivers[browserDeviceOptions.BrowserID] = driverExt + r.RegisterUIXTDriver(browserDeviceOptions.BrowserID, driverExt) } - return parsedConfig, nil + return 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 { From 0b1a92d8dd14c8654d8446382defe771d42b3830 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 15:24:12 +0800 Subject: [PATCH 11/19] refactor: NewCaseRunner --- internal/version/VERSION | 2 +- runner.go | 45 ++++++++++++-------------------------- step_testcase.go | 2 +- tests/runner_test.go | 2 +- tests/step_request_test.go | 2 +- 5 files changed, 18 insertions(+), 35 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index c3714433..9be87ef0 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505120918 +v5.0.0-beta-2505121525 diff --git a/runner.go b/runner.go index 7ea20de8..8877ede1 100644 --- a/runner.go +++ b/runner.go @@ -228,7 +228,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 @@ -276,12 +276,16 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) { return runErr } -// NewCaseRunner creates a new case runner for testcase. -// each testcase has its own case runner -func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { +// NewCaseRunner initializes a CaseRunner for a given testcase. +// Each testcase is associated with its own CaseRunner instance. +// 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 +293,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,39 +321,18 @@ 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 return caseRunner, nil } -func NewCaseRunner(testcase TestCase) (*CaseRunner, error) { - caseRunner := &CaseRunner{ - TestCase: testcase, - } - // config := testcase.Config.Get() - - // TODO: init parser plugin - - // parse testcase config - parsedConfig, err := caseRunner.parseConfig() - if err != nil { - return nil, errors.Wrap(err, "parse testcase config failed") - } - - // TODO: set request timeout in seconds - // TODO: set testcase timeout in seconds - - caseRunner.TestCase.Config = parsedConfig - return caseRunner, nil -} - type CaseRunner struct { TestCase // each testcase init its own CaseRunner diff --git a/step_testcase.go b/step_testcase.go index 83c4b668..f438c0ff 100644 --- a/step_testcase.go +++ b/step_testcase.go @@ -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 diff --git a/tests/runner_test.go b/tests/runner_test.go index e4745a5b..ba98f150 100644 --- a/tests/runner_test.go +++ b/tests/runner_test.go @@ -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)}") { diff --git a/tests/step_request_test.go b/tests/step_request_test.go index 7ba74678..3eba1459 100644 --- a/tests/step_request_test.go +++ b/tests/step_request_test.go @@ -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) From 00a6ca1f61bcbd4bc3ad0c1ab5d147fa521d203f Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 17:26:57 +0800 Subject: [PATCH 12/19] change: remove options for AppLaunch/AppTerminate/AppClear --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 15 +++------------ uixt/browser_driver.go | 14 +++----------- uixt/driver.go | 6 +++--- uixt/driver_action.go | 6 +++--- uixt/driver_handler.go | 8 -------- uixt/harmony_driver_hdc.go | 15 +++------------ uixt/ios_driver_wda.go | 15 +++------------ 8 files changed, 19 insertions(+), 62 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 9be87ef0..6e551f54 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505121525 +v5.0.0-beta-2505121727 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 80493956..2543cf91 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -266,15 +266,11 @@ func (ad *ADBDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error return } -func (ad *ADBDriver) AppLaunch(packageName string, opts ...option.ActionOption) (err error) { +func (ad *ADBDriver) AppLaunch(packageName string) (err error) { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppLaunch") // 不指定 Activity 名称启动(启动主 Activity) // adb shell monkey -p -c android.intent.category.LAUNCHER 1 - actionOptions := option.NewActionOptions(opts...) - preHandler_AppLaunch(ad, actionOptions) - defer postHandler(ad, actionOptions) - sOutput, err := ad.runShellCommand( "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1", ) @@ -289,15 +285,10 @@ func (ad *ADBDriver) AppLaunch(packageName string, opts ...option.ActionOption) return nil } -func (ad *ADBDriver) AppTerminate(packageName string, opts ...option.ActionOption) (successful bool, err error) { +func (ad *ADBDriver) AppTerminate(packageName string) (successful bool, err error) { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppTerminate") // 强制停止应用,停止 相关的进程 // adb shell am force-stop - - actionOptions := option.NewActionOptions(opts...) - preHandler_AppTerminate(ad, actionOptions) - defer postHandler(ad, actionOptions) - _, err = ad.runShellCommand("am", "force-stop", packageName) if err != nil { return false, errors.Wrap(err, "force-stop app failed") @@ -551,7 +542,7 @@ func (ad *ADBDriver) SendKeysByAdbKeyBoard(text string) (err error) { return } -func (ad *ADBDriver) AppClear(packageName string, opts ...option.ActionOption) error { +func (ad *ADBDriver) AppClear(packageName string) error { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppClear") if _, err := ad.runShellCommand("pm", "clear", packageName); err != nil { log.Error().Str("packageName", packageName).Err(err).Msg("failed to clear package cache") diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 81933236..044f3fb9 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -138,11 +138,7 @@ func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option. return } -func (wd *BrowserDriver) AppLaunch(packageName string, opts ...option.ActionOption) (err error) { - actionOptions := option.NewActionOptions(opts...) - preHandler_AppLaunch(wd, actionOptions) - defer postHandler(wd, actionOptions) - +func (wd *BrowserDriver) AppLaunch(packageName string) (err error) { data := map[string]interface{}{ "url": packageName, } @@ -465,11 +461,7 @@ func (wd *BrowserDriver) Unlock() (err error) { // AppTerminate Terminate an application with the given package name. // Either `true` if the app has been successfully terminated or `false` if it was not running -func (wd *BrowserDriver) AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) { - actionOptions := option.NewActionOptions(opts...) - preHandler_AppTerminate(wd, actionOptions) - defer postHandler(wd, actionOptions) - +func (wd *BrowserDriver) AppTerminate(packageName string) (bool, error) { return true, wd.DeleteSession() } @@ -482,7 +474,7 @@ func (wd *BrowserDriver) Back() error { return wd.PressBack() } -func (wd *BrowserDriver) AppClear(packageName string, opts ...option.ActionOption) error { +func (wd *BrowserDriver) AppClear(packageName string) error { return errors.New("not support") } diff --git a/uixt/driver.go b/uixt/driver.go index 5a6e7420..72d5ab06 100644 --- a/uixt/driver.go +++ b/uixt/driver.go @@ -69,9 +69,9 @@ type IDriver interface { Backspace(count int, opts ...option.ActionOption) error // app related - AppLaunch(packageName string, opts ...option.ActionOption) error - AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) - AppClear(packageName string, opts ...option.ActionOption) error + AppLaunch(packageName string) error + AppTerminate(packageName string) (bool, error) + AppClear(packageName string) error // image related PushImage(localPath string) error diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 767e27e4..aa63a211 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -145,13 +145,13 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { } case ACTION_AppClear: if packageName, ok := action.Params.(string); ok { - if err = dExt.AppClear(packageName, action.GetOptions()...); err != nil { + if err = dExt.AppClear(packageName); err != nil { return errors.Wrap(err, "failed to clear app") } } case ACTION_AppLaunch: if bundleId, ok := action.Params.(string); ok { - return dExt.AppLaunch(bundleId, action.GetOptions()...) + return dExt.AppLaunch(bundleId) } return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", ACTION_AppLaunch, action.Params) @@ -177,7 +177,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { return fmt.Errorf("invalid %s params: %v", ACTION_SwipeToTapTexts, action.Params) case ACTION_AppTerminate: if bundleId, ok := action.Params.(string); ok { - success, err := dExt.AppTerminate(bundleId, action.GetOptions()...) + success, err := dExt.AppTerminate(bundleId) if err != nil { return errors.Wrap(err, "failed to terminate app") } diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index 640b20eb..d8399429 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -114,13 +114,5 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra return fromX, fromY, toX, toY, nil } -func preHandler_AppLaunch(_ IDriver, options *option.ActionOptions) (err error) { - return nil -} - -func preHandler_AppTerminate(_ IDriver, options *option.ActionOptions) (err error) { - return nil -} - func postHandler(_ IDriver, options *option.ActionOptions) { } diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index 0719d33f..c12b831d 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -119,22 +119,13 @@ func (hd *HDCDriver) Unlock() (err error) { return hd.Swipe(500, 1500, 500, 500) } -func (hd *HDCDriver) AppLaunch(packageName string, opts ...option.ActionOption) error { - actionOptions := option.NewActionOptions(opts...) - preHandler_AppLaunch(hd, actionOptions) - defer postHandler(hd, actionOptions) - +func (hd *HDCDriver) AppLaunch(packageName string) error { // Todo return types.ErrDriverNotImplemented } -func (hd *HDCDriver) AppTerminate(packageName string, opts ...option.ActionOption) (bool, error) { +func (hd *HDCDriver) AppTerminate(packageName string) (bool, error) { log.Info().Str("packageName", packageName).Msg("HDCDriver.AppTerminate") - - actionOptions := option.NewActionOptions(opts...) - preHandler_AppTerminate(hd, actionOptions) - defer postHandler(hd, actionOptions) - _, err := hd.Device.RunShellCommand("aa", "force-stop", packageName) if err != nil { log.Error().Err(err).Msg("failed to terminal app") @@ -223,7 +214,7 @@ func (hd *HDCDriver) Input(text string, opts ...option.ActionOption) error { return hd.uiDriver.InputText(text) } -func (hd *HDCDriver) AppClear(packageName string, opts ...option.ActionOption) error { +func (hd *HDCDriver) AppClear(packageName string) error { return types.ErrDriverNotImplemented } diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 300c405d..f90406d5 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -491,14 +491,10 @@ func (wd *WDADriver) AlertSendKeys(text string) (err error) { return } -func (wd *WDADriver) AppLaunch(bundleId string, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) AppLaunch(bundleId string) (err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppLaunch") // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] - actionOptions := option.NewActionOptions(opts...) - preHandler_AppLaunch(wd, actionOptions) - defer postHandler(wd, actionOptions) - data := make(map[string]interface{}) data["bundleId"] = bundleId data["environment"] = map[string]interface{}{ @@ -525,14 +521,9 @@ func (wd *WDADriver) AppLaunchUnattached(bundleId string) (err error) { return nil } -func (wd *WDADriver) AppTerminate(bundleId string, opts ...option.ActionOption) (successful bool, err error) { +func (wd *WDADriver) AppTerminate(bundleId string) (successful bool, err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppTerminate") // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] - - actionOptions := option.NewActionOptions(opts...) - preHandler_AppTerminate(wd, actionOptions) - defer postHandler(wd, actionOptions) - data := map[string]interface{}{"bundleId": bundleId} var rawResp DriverRawResponse urlStr := fmt.Sprintf("/session/%s/wda/apps/terminate", wd.Session.ID) @@ -744,7 +735,7 @@ func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err erro return } -func (wd *WDADriver) AppClear(packageName string, opts ...option.ActionOption) error { +func (wd *WDADriver) AppClear(packageName string) error { return types.ErrDriverNotImplemented } From 0722e92e87ae53b5794fa0f05630a776dc4c24d7 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 17:31:08 +0800 Subject: [PATCH 13/19] change: remove options for AppLaunch/AppTerminate --- internal/version/VERSION | 2 +- step_ui.go | 14 ++++++-------- uixt/android_driver_adb.go | 1 - uixt/ios_driver_wda.go | 1 - 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 6e551f54..13685599 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505121727 +v5.0.0-beta-2505121736 diff --git a/step_ui.go b/step_ui.go index 1c3e5d38..ae3c2217 100644 --- a/step_ui.go +++ b/step_ui.go @@ -91,20 +91,18 @@ func (s *StepMobile) WebLoginNoneUI(packageName, phoneNumber string, captcha, pa return s } -func (s *StepMobile) AppLaunch(bundleId string, opts ...option.ActionOption) *StepMobile { +func (s *StepMobile) AppLaunch(bundleId string) *StepMobile { s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{ - Method: uixt.ACTION_AppLaunch, - Params: bundleId, - Options: option.NewActionOptions(opts...), + Method: uixt.ACTION_AppLaunch, + Params: bundleId, }) return s } -func (s *StepMobile) AppTerminate(bundleId string, opts ...option.ActionOption) *StepMobile { +func (s *StepMobile) AppTerminate(bundleId string) *StepMobile { s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{ - Method: uixt.ACTION_AppTerminate, - Params: bundleId, - Options: option.NewActionOptions(opts...), + Method: uixt.ACTION_AppTerminate, + Params: bundleId, }) return s } diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 2543cf91..0fa08110 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -270,7 +270,6 @@ func (ad *ADBDriver) AppLaunch(packageName string) (err error) { log.Info().Str("packageName", packageName).Msg("ADBDriver.AppLaunch") // 不指定 Activity 名称启动(启动主 Activity) // adb shell monkey -p -c android.intent.category.LAUNCHER 1 - sOutput, err := ad.runShellCommand( "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1", ) diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index f90406d5..2fc3b4f8 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -494,7 +494,6 @@ func (wd *WDADriver) AlertSendKeys(text string) (err error) { func (wd *WDADriver) AppLaunch(bundleId string) (err error) { log.Info().Str("bundleId", bundleId).Msg("WDADriver.AppLaunch") // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] - data := make(map[string]interface{}) data["bundleId"] = bundleId data["environment"] = map[string]interface{}{ From 9c735bd46aae2937840d3ed7ad243aef527cc097 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 17:59:51 +0800 Subject: [PATCH 14/19] feat: save screenshot after action --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 8 +++--- uixt/android_driver_uia2.go | 8 +++--- uixt/browser_driver.go | 6 ++-- uixt/driver_ext_ai.go | 4 +-- uixt/driver_ext_screenshot.go | 54 ++++++++++++++++------------------- uixt/driver_handler.go | 30 +++++++++++++++++-- uixt/harmony_driver_hdc.go | 4 +-- uixt/ios_driver_wda.go | 6 ++-- 9 files changed, 72 insertions(+), 50 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 13685599..e54be5be 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505121736 +v5.0.0-beta-2505121804 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 0fa08110..fbfe3d9b 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -312,7 +312,7 @@ func (ad *ADBDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { if err != nil { return err } - defer postHandler(ad, actionOptions) + defer postHandler(ad, ACTION_TapAbsXY, actionOptions) // adb shell input tap x y xStr := fmt.Sprintf("%.1f", x) @@ -331,7 +331,7 @@ func (ad *ADBDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error if err != nil { return err } - defer postHandler(ad, actionOptions) + defer postHandler(ad, ACTION_DoubleTapXY, actionOptions) // adb shell input tap x y xStr := fmt.Sprintf("%.1f", x) @@ -380,7 +380,7 @@ func (ad *ADBDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO if err != nil { return err } - defer postHandler(ad, actionOptions) + defer postHandler(ad, ACTION_Drag, actionOptions) duration := 200.0 if actionOptions.Duration > 0 { @@ -412,7 +412,7 @@ func (ad *ADBDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Action if err != nil { return err } - defer postHandler(ad, actionOptions) + defer postHandler(ad, ACTION_Swipe, actionOptions) // adb shell input swipe fromX fromY toX toY _, err = ad.runShellCommand( diff --git a/uixt/android_driver_uia2.go b/uixt/android_driver_uia2.go index 3b961a0f..320250cf 100644 --- a/uixt/android_driver_uia2.go +++ b/uixt/android_driver_uia2.go @@ -262,7 +262,7 @@ func (ud *UIA2Driver) DoubleTap(x, y float64, opts ...option.ActionOption) error if err != nil { return err } - defer postHandler(ud, actionOptions) + defer postHandler(ud, ACTION_DoubleTapXY, actionOptions) data := map[string]interface{}{ "actions": []interface{}{ @@ -304,7 +304,7 @@ func (ud *UIA2Driver) TapAbsXY(x, y float64, opts ...option.ActionOption) error if err != nil { return err } - defer postHandler(ud, actionOptions) + defer postHandler(ud, ACTION_TapAbsXY, actionOptions) duration := 100.0 if actionOptions.PressDuration > 0 { @@ -367,7 +367,7 @@ func (ud *UIA2Driver) Drag(fromX, fromY, toX, toY float64, opts ...option.Action if err != nil { return err } - defer postHandler(ud, actionOptions) + defer postHandler(ud, ACTION_Drag, actionOptions) data := map[string]interface{}{ "startX": fromX, @@ -398,7 +398,7 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio if err != nil { return err } - defer postHandler(ud, actionOptions) + defer postHandler(ud, ACTION_Swipe, actionOptions) duration := 200.0 if actionOptions.PressDuration > 0 { diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 044f3fb9..a0e349ef 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -119,7 +119,7 @@ func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option. if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_Drag, actionOptions) data := map[string]interface{}{ "from_x": fromX, @@ -518,7 +518,7 @@ func (wd *BrowserDriver) TapFloat(x, y float64, opts ...option.ActionOption) err if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_TapAbsXY, actionOptions) duration := 0.1 if actionOptions.Duration > 0 { @@ -542,7 +542,7 @@ func (wd *BrowserDriver) DoubleTap(x, y float64, options ...option.ActionOption) if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_DoubleTapXY, actionOptions) data := map[string]interface{}{ "x": x, diff --git a/uixt/driver_ext_ai.go b/uixt/driver_ext_ai.go index 286d90ee..61e0dc6a 100644 --- a/uixt/driver_ext_ai.go +++ b/uixt/driver_ext_ai.go @@ -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 } diff --git a/uixt/driver_ext_screenshot.go b/uixt/driver_ext_screenshot.go index fa10851b..ddb4c754 100644 --- a/uixt/driver_ext_screenshot.go +++ b/uixt/driver_ext_screenshot.go @@ -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)} diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index d8399429..e6ffa464 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -2,8 +2,11 @@ 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" ) @@ -104,7 +107,7 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra } fromX, fromY, toX, toY = options.ApplySwipeOffset(fromX, fromY, toX, toY) - // mark UI operation + // 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") @@ -114,5 +117,28 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra return fromX, fromY, toX, toY, nil } -func postHandler(_ IDriver, options *option.ActionOptions) { +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 } diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index c12b831d..cef8636d 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -159,7 +159,7 @@ func (hd *HDCDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { if err != nil { return err } - defer postHandler(hd, actionOptions) + defer postHandler(hd, ACTION_TapAbsXY, actionOptions) if actionOptions.Identifier != "" { startTime := int(time.Now().UnixMilli()) @@ -191,7 +191,7 @@ func (hd *HDCDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Action if err != nil { return err } - defer postHandler(hd, actionOptions) + defer postHandler(hd, ACTION_Swipe, actionOptions) duration := 200 if actionOptions.PressDuration > 0 { diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 2fc3b4f8..a0708ea9 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -602,7 +602,7 @@ func (wd *WDADriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_TapAbsXY, actionOptions) data := map[string]interface{}{ "x": x, @@ -627,7 +627,7 @@ func (wd *WDADriver) DoubleTap(x, y float64, opts ...option.ActionOption) error if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_DoubleTapXY, actionOptions) data := map[string]interface{}{ "x": x, @@ -664,7 +664,7 @@ func (wd *WDADriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO if err != nil { return err } - defer postHandler(wd, actionOptions) + defer postHandler(wd, ACTION_Drag, actionOptions) data := map[string]interface{}{ "fromX": math.Round(fromX*10) / 10, From 4d48a418f9e33097db4c29140ca4ae8317754eaf Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 19:16:01 +0800 Subject: [PATCH 15/19] change: parse mobile device config --- internal/version/VERSION | 2 +- runner.go | 75 ++++++++++++++++------------------------ 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index e54be5be..8d02c43d 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505121804 +v5.0.0-beta-2505121916 diff --git a/runner.go b/runner.go index 8877ede1..a4a50fdb 100644 --- a/runner.go +++ b/runner.go @@ -422,124 +422,109 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { } r.parametersIterator = parametersIterator - // register uixt drivers - if err := r.RegisterUIXTDrivers(parsedConfig); err != nil { - return nil, errors.Wrap(err, "register uixt drivers failed") - } - - return parsedConfig, nil -} - -func (r *CaseRunner) RegisterUIXTDrivers(config *TConfig) error { // ai options aiOpts := []option.AIServiceOption{} - if config.LLMService != "" { - aiOpts = append(aiOpts, option.WithLLMService(option.LLMServiceType(config.LLMService))) + if parsedConfig.LLMService != "" { + aiOpts = append(aiOpts, option.WithLLMService(option.LLMServiceType(parsedConfig.LLMService))) } - if config.CVService == "" { + if parsedConfig.CVService == "" { // default to vedem - config.CVService = option.CVServiceTypeVEDEM + parsedConfig.CVService = option.CVServiceTypeVEDEM } - aiOpts = append(aiOpts, option.WithCVService(config.CVService)) + aiOpts = append(aiOpts, option.WithCVService(parsedConfig.CVService)) // parse android devices config - for _, androidDeviceOptions := range config.Android { - err := r.parseDeviceConfig(androidDeviceOptions, config.Variables) + for _, androidDeviceOptions := range parsedConfig.Android { + err := r.parseDeviceConfig(androidDeviceOptions, parsedConfig.Variables) if err != nil { - return errors.Wrap(code.InvalidCaseError, + return nil, errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse android config failed: %v", err)) } device, err := uixt.NewAndroidDevice(androidDeviceOptions.Options()...) if err != nil { - return errors.Wrap(err, "init android device failed") + return nil, errors.Wrap(err, "init android device failed") } driver, err := device.NewDriver() if err != nil { - return errors.Wrap(err, "init android driver failed") + return nil, errors.Wrap(err, "init android driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return errors.Wrap(err, "init android XTDriver failed") + return nil, errors.Wrap(err, "init android XTDriver failed") } r.RegisterUIXTDriver(androidDeviceOptions.SerialNumber, driverExt) } // parse iOS devices config - for _, iosDeviceOptions := range config.IOS { - err := r.parseDeviceConfig(iosDeviceOptions, config.Variables) + for _, iosDeviceOptions := range parsedConfig.IOS { + err := r.parseDeviceConfig(iosDeviceOptions, parsedConfig.Variables) if err != nil { - return errors.Wrap(code.InvalidCaseError, + return nil, errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse ios config failed: %v", err)) } device, err := uixt.NewIOSDevice(iosDeviceOptions.Options()...) if err != nil { - return errors.Wrap(err, "init ios device failed") + return nil, errors.Wrap(err, "init ios device failed") } driver, err := device.NewDriver() if err != nil { - return errors.Wrap(err, "init ios driver failed") + return nil, errors.Wrap(err, "init ios driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return errors.Wrap(err, "init ios XTDriver failed") + return nil, errors.Wrap(err, "init ios XTDriver failed") } r.RegisterUIXTDriver(iosDeviceOptions.UDID, driverExt) } // parse harmony devices config - for _, harmonyDeviceOptions := range config.Harmony { - err := r.parseDeviceConfig(harmonyDeviceOptions, config.Variables) + for _, harmonyDeviceOptions := range parsedConfig.Harmony { + err := r.parseDeviceConfig(harmonyDeviceOptions, parsedConfig.Variables) if err != nil { - return errors.Wrap(code.InvalidCaseError, + return nil, errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse harmony config failed: %v", err)) } device, err := uixt.NewHarmonyDevice(harmonyDeviceOptions.Options()...) if err != nil { - return errors.Wrap(err, "init harmony device failed") + return nil, errors.Wrap(err, "init harmony device failed") } driver, err := device.NewDriver() if err != nil { - return errors.Wrap(err, "init harmony driver failed") + return nil, errors.Wrap(err, "init harmony driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return errors.Wrap(err, "init harmony XTDriver failed") + return nil, errors.Wrap(err, "init harmony XTDriver failed") } r.RegisterUIXTDriver(harmonyDeviceOptions.ConnectKey, driverExt) } // parse browser devices config - for _, browserDeviceOptions := range config.Browser { - err := r.parseDeviceConfig(browserDeviceOptions, config.Variables) + for _, browserDeviceOptions := range parsedConfig.Browser { + err := r.parseDeviceConfig(browserDeviceOptions, parsedConfig.Variables) if err != nil { - return errors.Wrap(code.InvalidCaseError, + return nil, errors.Wrap(code.InvalidCaseError, fmt.Sprintf("parse browser config failed: %v", err)) } device, err := uixt.NewBrowserDevice(browserDeviceOptions.Options()...) if err != nil { - return errors.Wrap(err, "init browser device failed") - } - if err := device.Setup(); err != nil { - return err + return nil, errors.Wrap(err, "init browser device failed") } driver, err := device.NewDriver() if err != nil { - return errors.Wrap(err, "init browser driver failed") - } - if err := driver.Setup(); err != nil { - return errors.Wrap(err, "init browser driver failed") + return nil, errors.Wrap(err, "init browser driver failed") } driverExt, err := uixt.NewXTDriver(driver, aiOpts...) if err != nil { - return errors.Wrap(err, "init browser XTDriver failed") + return nil, errors.Wrap(err, "init browser XTDriver failed") } r.RegisterUIXTDriver(browserDeviceOptions.BrowserID, driverExt) } - return nil + return parsedConfig, nil } func (r *CaseRunner) RegisterUIXTDriver(serial string, driver *uixt.XTDriver) { From 3b3807770d74da5edc1345614c2b28ca18074c99 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 21:35:49 +0800 Subject: [PATCH 16/19] change: update docstring --- internal/version/VERSION | 2 +- runner.go | 4 ++-- uixt/driver_action.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 8d02c43d..d3b09107 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505121916 +v5.0.0-beta-2505122135 diff --git a/runner.go b/runner.go index a4a50fdb..a5c18f6e 100644 --- a/runner.go +++ b/runner.go @@ -276,8 +276,8 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) { return runErr } -// NewCaseRunner initializes a CaseRunner for a given testcase. -// Each testcase is associated with its own CaseRunner instance. +// NewCaseRunner creates a new case runner for testcase. +// each testcase has its own case runner // 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 { diff --git a/uixt/driver_action.go b/uixt/driver_action.go index aa63a211..5b9b1c2e 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -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 } From 6f428ce5cdc81c51e494c27f68ea6cca96c973ff Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 12 May 2025 22:46:21 +0800 Subject: [PATCH 17/19] change: upgrade mcp-go to v0.27.0 --- go.mod | 2 +- go.sum | 4 ++-- internal/mcp/hub.go | 22 ++++++++++++---------- internal/version/VERSION | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index dd685368..e59c1087 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 86ab0411..2b927b36 100644 --- a/go.sum +++ b/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= diff --git a/internal/mcp/hub.go b/internal/mcp/hub.go index 7f2000bc..521af463 100644 --- a/internal/mcp/hub.go +++ b/internal/mcp/hub.go @@ -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 diff --git a/internal/version/VERSION b/internal/version/VERSION index d3b09107..db6e693f 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505122135 +v5.0.0-beta-2505122246 From d145784910e25ce2d8db14447dc2a8ffc90a030a Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 14 May 2025 14:36:46 +0800 Subject: [PATCH 18/19] fix: swipe with params --- compat.go | 3 -- internal/builtin/utils.go | 4 +- internal/builtin/utils_test.go | 96 ++++++++++++++++++++++++++++++++++ internal/version/VERSION | 2 +- uixt/driver_action.go | 7 +++ uixt/option/action.go | 21 ++++++-- 6 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 internal/builtin/utils_test.go diff --git a/compat.go b/compat.go index 22976841..ba924c54 100644 --- a/compat.go +++ b/compat.go @@ -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 } } diff --git a/internal/builtin/utils.go b/internal/builtin/utils.go index 638fcef5..1ef6785b 100644 --- a/internal/builtin/utils.go +++ b/internal/builtin/utils.go @@ -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) diff --git a/internal/builtin/utils_test.go b/internal/builtin/utils_test.go new file mode 100644 index 00000000..c115a472 --- /dev/null +++ b/internal/builtin/utils_test.go @@ -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) + } + }) + } +} diff --git a/internal/version/VERSION b/internal/version/VERSION index db6e693f..7e839094 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505122246 +v5.0.0-beta-2505141436 diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 5b9b1c2e..810b7233 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -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: diff --git a/uixt/option/action.go b/uixt/option/action.go index 81911ec8..ac3ca847 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -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, From c71ac5c3cd46c2865c2ba9222b78106eef3e5cf7 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 14 May 2025 14:49:05 +0800 Subject: [PATCH 19/19] feat: set step loops with expression variable --- internal/version/VERSION | 2 +- runner.go | 48 +++++++++++++++--- step.go | 7 ++- step_request.go | 5 +- uixt/types/field.go | 107 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 uixt/types/field.go diff --git a/internal/version/VERSION b/internal/version/VERSION index 7e839094..5d530a5f 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505141436 +v5.0.0-beta-2505141501 diff --git a/runner.go b/runner.go index a5c18f6e..2a6f3880 100644 --- a/runner.go +++ b/runner.go @@ -11,6 +11,7 @@ import ( "os" "os/signal" "reflect" + "strconv" "strings" "syscall" "testing" @@ -707,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 @@ -840,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 +} diff --git a/step.go b/step.go index ac6e0f37..b1cd9d16 100644 --- a/step.go +++ b/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"` } diff --git a/step_request.go b/step_request.go index 983f6e92..a596bb6c 100644 --- a/step_request.go +++ b/step_request.go @@ -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 } diff --git a/uixt/types/field.go b/uixt/types/field.go new file mode 100644 index 00000000..2835f670 --- /dev/null +++ b/uixt/types/field.go @@ -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)) +}