mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-28 02:51:42 +08:00
refactor: move action options
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/httprunner/httprunner/v5/code"
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
@@ -138,17 +137,17 @@ func convertCompatMobileStep(mobileUI *MobileUI) {
|
||||
ma := mobileUI.Actions[i]
|
||||
actionOptions := option.NewActionOptions(ma.GetOptions()...)
|
||||
// append tap_cv params to screenshot_with_ui_types option
|
||||
if ma.Method == uixt.ACTION_TapByCV {
|
||||
if ma.Method == option.ACTION_TapByCV {
|
||||
uiTypes, _ := builtin.ConvertToStringSlice(ma.Params)
|
||||
ma.ActionOptions.ScreenShotWithUITypes = append(ma.ActionOptions.ScreenShotWithUITypes, uiTypes...)
|
||||
ma.ActionOptions.ScreenShotWithUpload = true
|
||||
}
|
||||
// set default max_retry_times to 10 for swipe_to_tap_texts
|
||||
if ma.Method == uixt.ACTION_SwipeToTapTexts && actionOptions.MaxRetryTimes == 0 {
|
||||
if ma.Method == option.ACTION_SwipeToTapTexts && actionOptions.MaxRetryTimes == 0 {
|
||||
ma.ActionOptions.MaxRetryTimes = 10
|
||||
}
|
||||
// set default max_retry_times to 10 for swipe_to_tap_text
|
||||
if ma.Method == uixt.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
||||
if ma.Method == option.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
||||
ma.ActionOptions.MaxRetryTimes = 10
|
||||
}
|
||||
mobileUI.Actions[i] = ma
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-beta-2505242351
|
||||
v5.0.0-beta-2505250015
|
||||
|
||||
126
step_ui.go
126
step_ui.go
@@ -67,9 +67,9 @@ func (s *StepMobile) Serial(serial string) *StepMobile {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepMobile) Log(actionName uixt.ActionMethod) *StepMobile {
|
||||
func (s *StepMobile) Log(actionName option.ActionMethod) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_LOG,
|
||||
Method: option.ACTION_LOG,
|
||||
Params: actionName,
|
||||
})
|
||||
return s
|
||||
@@ -77,7 +77,7 @@ func (s *StepMobile) Log(actionName uixt.ActionMethod) *StepMobile {
|
||||
|
||||
func (s *StepMobile) InstallApp(path string) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_AppInstall,
|
||||
Method: option.ACTION_AppInstall,
|
||||
Params: path,
|
||||
})
|
||||
return s
|
||||
@@ -85,7 +85,7 @@ func (s *StepMobile) InstallApp(path string) *StepMobile {
|
||||
|
||||
func (s *StepMobile) WebLoginNoneUI(packageName, phoneNumber string, captcha, password string) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_WebLoginNoneUI,
|
||||
Method: option.ACTION_WebLoginNoneUI,
|
||||
Params: []string{packageName, phoneNumber, captcha, password},
|
||||
})
|
||||
return s
|
||||
@@ -93,7 +93,7 @@ func (s *StepMobile) WebLoginNoneUI(packageName, phoneNumber string, captcha, pa
|
||||
|
||||
func (s *StepMobile) AppLaunch(bundleId string) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_AppLaunch,
|
||||
Method: option.ACTION_AppLaunch,
|
||||
Params: bundleId,
|
||||
})
|
||||
return s
|
||||
@@ -101,7 +101,7 @@ func (s *StepMobile) AppLaunch(bundleId string) *StepMobile {
|
||||
|
||||
func (s *StepMobile) AppTerminate(bundleId string) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_AppTerminate,
|
||||
Method: option.ACTION_AppTerminate,
|
||||
Params: bundleId,
|
||||
})
|
||||
return s
|
||||
@@ -109,7 +109,7 @@ func (s *StepMobile) AppTerminate(bundleId string) *StepMobile {
|
||||
|
||||
func (s *StepMobile) Home() *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_Home,
|
||||
Method: option.ACTION_Home,
|
||||
Params: nil,
|
||||
})
|
||||
return s
|
||||
@@ -120,7 +120,7 @@ func (s *StepMobile) Home() *StepMobile {
|
||||
// else, X & Y will be considered as absolute coordinates
|
||||
func (s *StepMobile) TapXY(x, y float64, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapXY,
|
||||
Method: option.ACTION_TapXY,
|
||||
Params: []float64{x, y},
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func (s *StepMobile) TapXY(x, y float64, opts ...option.ActionOption) *StepMobil
|
||||
// TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates
|
||||
func (s *StepMobile) TapAbsXY(x, y float64, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapAbsXY,
|
||||
Method: option.ACTION_TapAbsXY,
|
||||
Params: []float64{x, y},
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (s *StepMobile) TapAbsXY(x, y float64, opts ...option.ActionOption) *StepMo
|
||||
// TapByOCR taps on the target element by OCR recognition
|
||||
func (s *StepMobile) TapByOCR(ocrText string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapByOCR,
|
||||
Method: option.ACTION_TapByOCR,
|
||||
Params: ocrText,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (s *StepMobile) TapByOCR(ocrText string, opts ...option.ActionOption) *Step
|
||||
// TapByCV taps on the target element by CV recognition
|
||||
func (s *StepMobile) TapByCV(imagePath string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapByCV,
|
||||
Method: option.ACTION_TapByCV,
|
||||
Params: imagePath,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (s *StepMobile) TapByCV(imagePath string, opts ...option.ActionOption) *Ste
|
||||
// TapByUITypes taps on the target element specified by uiTypes, the higher the uiTypes, the higher the priority
|
||||
func (s *StepMobile) TapByUITypes(opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapByCV,
|
||||
Method: option.ACTION_TapByCV,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (s *StepMobile) TapByUITypes(opts ...option.ActionOption) *StepMobile {
|
||||
// AIAction do actions with VLM
|
||||
func (s *StepMobile) AIAction(prompt string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_AIAction,
|
||||
Method: option.ACTION_AIAction,
|
||||
Params: prompt,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -191,7 +191,7 @@ func (s *StepMobile) AIAction(prompt string, opts ...option.ActionOption) *StepM
|
||||
// DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates
|
||||
func (s *StepMobile) DoubleTapXY(x, y float64, opts ...option.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_DoubleTapXY,
|
||||
Method: option.ACTION_DoubleTapXY,
|
||||
Params: []float64{x, y},
|
||||
Options: option.NewActionOptions(opts...),
|
||||
})
|
||||
@@ -200,7 +200,7 @@ func (s *StepMobile) DoubleTapXY(x, y float64, opts ...option.ActionOption) *Ste
|
||||
|
||||
func (s *StepMobile) Back() *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Back,
|
||||
Method: option.ACTION_Back,
|
||||
Params: nil,
|
||||
Options: nil,
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func (s *StepMobile) Back() *StepMobile {
|
||||
// Swipe drags from [sx, sy] to [ex, ey]
|
||||
func (s *StepMobile) Swipe(sx, sy, ex, ey float64, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Swipe,
|
||||
Method: option.ACTION_Swipe,
|
||||
Params: []float64{sx, sy, ex, ey},
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (s *StepMobile) Swipe(sx, sy, ex, ey float64, opts ...option.ActionOption)
|
||||
|
||||
func (s *StepMobile) SwipeUp(opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Swipe,
|
||||
Method: option.ACTION_Swipe,
|
||||
Params: "up",
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func (s *StepMobile) SwipeUp(opts ...option.ActionOption) *StepMobile {
|
||||
|
||||
func (s *StepMobile) SwipeDown(opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Swipe,
|
||||
Method: option.ACTION_Swipe,
|
||||
Params: "down",
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (s *StepMobile) SwipeDown(opts ...option.ActionOption) *StepMobile {
|
||||
|
||||
func (s *StepMobile) SwipeLeft(opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Swipe,
|
||||
Method: option.ACTION_Swipe,
|
||||
Params: "left",
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func (s *StepMobile) SwipeLeft(opts ...option.ActionOption) *StepMobile {
|
||||
|
||||
func (s *StepMobile) SwipeRight(opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Swipe,
|
||||
Method: option.ACTION_Swipe,
|
||||
Params: "right",
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -267,7 +267,7 @@ func (s *StepMobile) SwipeRight(opts ...option.ActionOption) *StepMobile {
|
||||
|
||||
func (s *StepMobile) SwipeToTapApp(appName string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SwipeToTapApp,
|
||||
Method: option.ACTION_SwipeToTapApp,
|
||||
Params: appName,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func (s *StepMobile) SwipeToTapApp(appName string, opts ...option.ActionOption)
|
||||
|
||||
func (s *StepMobile) SwipeToTapText(text string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SwipeToTapText,
|
||||
Method: option.ACTION_SwipeToTapText,
|
||||
Params: text,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func (s *StepMobile) SwipeToTapText(text string, opts ...option.ActionOption) *S
|
||||
|
||||
func (s *StepMobile) SwipeToTapTexts(texts interface{}, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SwipeToTapTexts,
|
||||
Method: option.ACTION_SwipeToTapTexts,
|
||||
Params: texts,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -300,7 +300,7 @@ func (s *StepMobile) SwipeToTapTexts(texts interface{}, opts ...option.ActionOpt
|
||||
|
||||
func (s *StepMobile) SecondaryClick(x, y float64, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SecondaryClick,
|
||||
Method: option.ACTION_SecondaryClick,
|
||||
Params: []float64{x, y},
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -310,7 +310,7 @@ func (s *StepMobile) SecondaryClick(x, y float64, options ...option.ActionOption
|
||||
|
||||
func (s *StepMobile) SecondaryClickBySelector(selector string, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SecondaryClickBySelector,
|
||||
Method: option.ACTION_SecondaryClickBySelector,
|
||||
Params: selector,
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func (s *StepMobile) SecondaryClickBySelector(selector string, options ...option
|
||||
|
||||
func (s *StepMobile) HoverBySelector(selector string, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_HoverBySelector,
|
||||
Method: option.ACTION_HoverBySelector,
|
||||
Params: selector,
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -330,7 +330,7 @@ func (s *StepMobile) HoverBySelector(selector string, options ...option.ActionOp
|
||||
|
||||
func (s *StepMobile) TapBySelector(selector string, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_TapBySelector,
|
||||
Method: option.ACTION_TapBySelector,
|
||||
Params: selector,
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -340,7 +340,7 @@ func (s *StepMobile) TapBySelector(selector string, options ...option.ActionOpti
|
||||
|
||||
func (s *StepMobile) WebCloseTab(idx int, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_WebCloseTab,
|
||||
Method: option.ACTION_WebCloseTab,
|
||||
Params: idx,
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func (s *StepMobile) WebCloseTab(idx int, options ...option.ActionOption) *StepM
|
||||
|
||||
func (s *StepMobile) GetElementTextBySelector(selector string, options ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_GetElementTextBySelector,
|
||||
Method: option.ACTION_GetElementTextBySelector,
|
||||
Params: selector,
|
||||
Options: option.NewActionOptions(options...),
|
||||
}
|
||||
@@ -360,7 +360,7 @@ func (s *StepMobile) GetElementTextBySelector(selector string, options ...option
|
||||
|
||||
func (s *StepMobile) Input(text string, opts ...option.ActionOption) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Input,
|
||||
Method: option.ACTION_Input,
|
||||
Params: text,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
}
|
||||
@@ -372,7 +372,7 @@ func (s *StepMobile) Input(text string, opts ...option.ActionOption) *StepMobile
|
||||
// Sleep specify sleep seconds after last action
|
||||
func (s *StepMobile) Sleep(nSeconds float64, startTime ...time.Time) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_Sleep,
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: nSeconds,
|
||||
Options: nil,
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func (s *StepMobile) Sleep(nSeconds float64, startTime ...time.Time) *StepMobile
|
||||
|
||||
func (s *StepMobile) SleepMS(nMilliseconds int64, startTime ...time.Time) *StepMobile {
|
||||
action := uixt.MobileAction{
|
||||
Method: uixt.ACTION_SleepMS,
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: nMilliseconds,
|
||||
Options: nil,
|
||||
}
|
||||
@@ -408,7 +408,7 @@ func (s *StepMobile) SleepMS(nMilliseconds int64, startTime ...time.Time) *StepM
|
||||
// 2. [min1, max1, weight1, min2, max2, weight2, ...] : weight is the probability of the time range
|
||||
func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_SleepRandom,
|
||||
Method: option.ACTION_SleepRandom,
|
||||
Params: params,
|
||||
Options: nil,
|
||||
})
|
||||
@@ -417,7 +417,7 @@ func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
|
||||
|
||||
func (s *StepMobile) EndToEndDelay(opts ...option.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_EndToEndDelay,
|
||||
Method: option.ACTION_EndToEndDelay,
|
||||
Params: nil,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
})
|
||||
@@ -426,7 +426,7 @@ func (s *StepMobile) EndToEndDelay(opts ...option.ActionOption) *StepMobile {
|
||||
|
||||
func (s *StepMobile) ScreenShot(opts ...option.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_ScreenShot,
|
||||
Method: option.ACTION_ScreenShot,
|
||||
Params: nil,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
})
|
||||
@@ -440,7 +440,7 @@ func (s *StepMobile) DisableAutoPopupHandler() *StepMobile {
|
||||
|
||||
func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
|
||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||
Method: uixt.ACTION_ClosePopups,
|
||||
Method: option.ACTION_ClosePopups,
|
||||
Params: nil,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
})
|
||||
@@ -449,7 +449,7 @@ func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *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,
|
||||
Method: option.ACTION_CallFunction,
|
||||
Params: name, // function description
|
||||
Fn: fn,
|
||||
Options: option.NewActionOptions(opts...),
|
||||
@@ -493,8 +493,8 @@ type StepMobileUIValidation struct {
|
||||
|
||||
func (s *StepMobileUIValidation) AssertNameExists(expectedName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorName,
|
||||
Assert: uixt.AssertionExists,
|
||||
Check: option.SelectorName,
|
||||
Assert: option.AssertionExists,
|
||||
Expect: expectedName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -508,8 +508,8 @@ func (s *StepMobileUIValidation) AssertNameExists(expectedName string, msg ...st
|
||||
|
||||
func (s *StepMobileUIValidation) AssertNameNotExists(expectedName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorName,
|
||||
Assert: uixt.AssertionNotExists,
|
||||
Check: option.SelectorName,
|
||||
Assert: option.AssertionNotExists,
|
||||
Expect: expectedName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -523,8 +523,8 @@ func (s *StepMobileUIValidation) AssertNameNotExists(expectedName string, msg ..
|
||||
|
||||
func (s *StepMobileUIValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorLabel,
|
||||
Assert: uixt.AssertionExists,
|
||||
Check: option.SelectorLabel,
|
||||
Assert: option.AssertionExists,
|
||||
Expect: expectedLabel,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -538,8 +538,8 @@ func (s *StepMobileUIValidation) AssertLabelExists(expectedLabel string, msg ...
|
||||
|
||||
func (s *StepMobileUIValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorLabel,
|
||||
Assert: uixt.AssertionNotExists,
|
||||
Check: option.SelectorLabel,
|
||||
Assert: option.AssertionNotExists,
|
||||
Expect: expectedLabel,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -553,8 +553,8 @@ func (s *StepMobileUIValidation) AssertLabelNotExists(expectedLabel string, msg
|
||||
|
||||
func (s *StepMobileUIValidation) AssertOCRExists(expectedText string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorOCR,
|
||||
Assert: uixt.AssertionExists,
|
||||
Check: option.SelectorOCR,
|
||||
Assert: option.AssertionExists,
|
||||
Expect: expectedText,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -568,8 +568,8 @@ func (s *StepMobileUIValidation) AssertOCRExists(expectedText string, msg ...str
|
||||
|
||||
func (s *StepMobileUIValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorOCR,
|
||||
Assert: uixt.AssertionNotExists,
|
||||
Check: option.SelectorOCR,
|
||||
Assert: option.AssertionNotExists,
|
||||
Expect: expectedText,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -583,8 +583,8 @@ func (s *StepMobileUIValidation) AssertOCRNotExists(expectedText string, msg ...
|
||||
|
||||
func (s *StepMobileUIValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorImage,
|
||||
Assert: uixt.AssertionExists,
|
||||
Check: option.SelectorImage,
|
||||
Assert: option.AssertionExists,
|
||||
Expect: expectedImagePath,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -598,8 +598,8 @@ func (s *StepMobileUIValidation) AssertImageExists(expectedImagePath string, msg
|
||||
|
||||
func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorImage,
|
||||
Assert: uixt.AssertionNotExists,
|
||||
Check: option.SelectorImage,
|
||||
Assert: option.AssertionNotExists,
|
||||
Expect: expectedImagePath,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -613,8 +613,8 @@ func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string,
|
||||
|
||||
func (s *StepMobileUIValidation) AssertAI(prompt string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorAI,
|
||||
Assert: uixt.AssertionAI,
|
||||
Check: option.SelectorAI,
|
||||
Assert: option.AssertionAI,
|
||||
Expect: prompt,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -628,8 +628,8 @@ func (s *StepMobileUIValidation) AssertAI(prompt string, msg ...string) *StepMob
|
||||
|
||||
func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorForegroundApp,
|
||||
Assert: uixt.AssertionEqual,
|
||||
Check: option.SelectorForegroundApp,
|
||||
Assert: option.AssertionEqual,
|
||||
Expect: packageName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -643,8 +643,8 @@ func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg .
|
||||
|
||||
func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorForegroundApp,
|
||||
Assert: uixt.AssertionNotEqual,
|
||||
Check: option.SelectorForegroundApp,
|
||||
Assert: option.AssertionNotEqual,
|
||||
Expect: packageName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
@@ -739,7 +739,7 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
||||
startTime := time.Now()
|
||||
actionResult := &ActionResult{
|
||||
MobileAction: uixt.MobileAction{
|
||||
Method: uixt.ACTION_GetForegroundApp,
|
||||
Method: option.ACTION_GetForegroundApp,
|
||||
Params: "[ForDebug] check foreground app",
|
||||
},
|
||||
StartTime: startTime.Unix(),
|
||||
@@ -758,7 +758,7 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
||||
startTime := time.Now()
|
||||
actionResult := &ActionResult{
|
||||
MobileAction: uixt.MobileAction{
|
||||
Method: uixt.ACTION_ClosePopups,
|
||||
Method: option.ACTION_ClosePopups,
|
||||
Params: "[ForDebug] close popups handler",
|
||||
},
|
||||
StartTime: startTime.Unix(),
|
||||
@@ -802,9 +802,9 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
||||
}
|
||||
|
||||
// stat uixt action
|
||||
if action.Method == uixt.ACTION_LOG {
|
||||
if action.Method == option.ACTION_LOG {
|
||||
log.Info().Interface("action", action.Params).Msg("stat uixt action")
|
||||
actionMethod := uixt.ActionMethod(action.Params.(string))
|
||||
actionMethod := option.ActionMethod(action.Params.(string))
|
||||
s.summary.Stat.Actions[actionMethod]++
|
||||
continue
|
||||
}
|
||||
|
||||
14
summary.go
14
summary.go
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func NewSummary() *Summary {
|
||||
@@ -28,7 +28,7 @@ func NewSummary() *Summary {
|
||||
Success: true,
|
||||
Stat: &Stat{
|
||||
TestSteps: TestStepStat{
|
||||
Actions: make(map[uixt.ActionMethod]int),
|
||||
Actions: make(map[option.ActionMethod]int),
|
||||
},
|
||||
},
|
||||
Time: &TestCaseTime{
|
||||
@@ -146,10 +146,10 @@ type TestCaseStat struct {
|
||||
}
|
||||
|
||||
type TestStepStat struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Successes int `json:"successes" yaml:"successes"`
|
||||
Failures int `json:"failures" yaml:"failures"`
|
||||
Actions map[uixt.ActionMethod]int `json:"actions" yaml:"actions"` // record action stats
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Successes int `json:"successes" yaml:"successes"`
|
||||
Failures int `json:"failures" yaml:"failures"`
|
||||
Actions map[option.ActionMethod]int `json:"actions" yaml:"actions"` // record action stats
|
||||
}
|
||||
|
||||
type TestCaseTime struct {
|
||||
@@ -167,7 +167,7 @@ func NewCaseSummary() *TestCaseSummary {
|
||||
return &TestCaseSummary{
|
||||
Success: true,
|
||||
Stat: &TestStepStat{
|
||||
Actions: make(map[uixt.ActionMethod]int),
|
||||
Actions: make(map[option.ActionMethod]int),
|
||||
},
|
||||
Time: &TestCaseTime{
|
||||
StartAt: time.Now(),
|
||||
|
||||
@@ -312,7 +312,7 @@ func (ad *ADBDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ad, ACTION_TapAbsXY, actionOptions)
|
||||
defer postHandler(ad, option.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, ACTION_DoubleTapXY, actionOptions)
|
||||
defer postHandler(ad, option.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, ACTION_Drag, actionOptions)
|
||||
defer postHandler(ad, option.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, ACTION_Swipe, actionOptions)
|
||||
defer postHandler(ad, option.ACTION_Swipe, actionOptions)
|
||||
|
||||
// adb shell input swipe fromX fromY toX toY
|
||||
_, err = ad.runShellCommand(
|
||||
|
||||
@@ -262,7 +262,7 @@ func (ud *UIA2Driver) DoubleTap(x, y float64, opts ...option.ActionOption) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(ud, ACTION_DoubleTapXY, actionOptions)
|
||||
defer postHandler(ud, option.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, ACTION_TapAbsXY, actionOptions)
|
||||
defer postHandler(ud, option.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, ACTION_Drag, actionOptions)
|
||||
defer postHandler(ud, option.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, ACTION_Swipe, actionOptions)
|
||||
defer postHandler(ud, option.ACTION_Swipe, actionOptions)
|
||||
|
||||
duration := 200.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
|
||||
@@ -119,7 +119,7 @@ func (wd *BrowserDriver) Drag(fromX, fromY, toX, toY float64, options ...option.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_Drag, actionOptions)
|
||||
defer postHandler(wd, option.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, ACTION_TapAbsXY, actionOptions)
|
||||
defer postHandler(wd, option.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, ACTION_DoubleTapXY, actionOptions)
|
||||
defer postHandler(wd, option.ACTION_DoubleTapXY, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
|
||||
@@ -14,78 +14,8 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
type ActionMethod string
|
||||
|
||||
const (
|
||||
ACTION_LOG ActionMethod = "log"
|
||||
ACTION_AppInstall ActionMethod = "install"
|
||||
ACTION_AppUninstall ActionMethod = "uninstall"
|
||||
ACTION_WebLoginNoneUI ActionMethod = "login_none_ui"
|
||||
ACTION_AppClear ActionMethod = "app_clear"
|
||||
ACTION_AppStart ActionMethod = "app_start"
|
||||
ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
|
||||
ACTION_AppTerminate ActionMethod = "app_terminate"
|
||||
ACTION_AppStop ActionMethod = "app_stop"
|
||||
ACTION_ScreenShot ActionMethod = "screenshot"
|
||||
ACTION_Sleep ActionMethod = "sleep"
|
||||
ACTION_SleepMS ActionMethod = "sleep_ms"
|
||||
ACTION_SleepRandom ActionMethod = "sleep_random"
|
||||
ACTION_SetIme ActionMethod = "set_ime"
|
||||
ACTION_GetSource ActionMethod = "get_source"
|
||||
ACTION_GetForegroundApp ActionMethod = "get_foreground_app"
|
||||
ACTION_CallFunction ActionMethod = "call_function"
|
||||
|
||||
// UI handling
|
||||
ACTION_Home ActionMethod = "home"
|
||||
ACTION_TapXY ActionMethod = "tap_xy"
|
||||
ACTION_TapAbsXY ActionMethod = "tap_abs_xy"
|
||||
ACTION_TapByOCR ActionMethod = "tap_ocr"
|
||||
ACTION_TapByCV ActionMethod = "tap_cv"
|
||||
ACTION_DoubleTapXY ActionMethod = "double_tap_xy"
|
||||
ACTION_Swipe ActionMethod = "swipe"
|
||||
ACTION_Drag ActionMethod = "drag"
|
||||
ACTION_Input ActionMethod = "input"
|
||||
ACTION_Back ActionMethod = "back"
|
||||
ACTION_KeyCode ActionMethod = "keycode"
|
||||
ACTION_AIAction ActionMethod = "ai_action" // action with ai
|
||||
ACTION_TapBySelector ActionMethod = "tap_by_selector"
|
||||
ACTION_HoverBySelector ActionMethod = "hover_by_selector"
|
||||
ACTION_WebCloseTab ActionMethod = "web_close_tab"
|
||||
ACTION_SecondaryClick ActionMethod = "secondary_click"
|
||||
ACTION_SecondaryClickBySelector ActionMethod = "secondary_click_by_selector"
|
||||
ACTION_GetElementTextBySelector ActionMethod = "get_element_text_by_selector"
|
||||
|
||||
// custom actions
|
||||
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
|
||||
ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
|
||||
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
||||
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
||||
ACTION_InstallApp ActionMethod = "install_app"
|
||||
ACTION_UninstallApp ActionMethod = "uninstall_app"
|
||||
ACTION_DownloadApp ActionMethod = "download_app"
|
||||
)
|
||||
|
||||
const (
|
||||
// UI validation
|
||||
// selectors
|
||||
SelectorName string = "ui_name"
|
||||
SelectorLabel string = "ui_label"
|
||||
SelectorOCR string = "ui_ocr"
|
||||
SelectorImage string = "ui_image"
|
||||
SelectorAI string = "ui_ai" // ui query with ai
|
||||
SelectorForegroundApp string = "ui_foreground_app"
|
||||
SelectorSelector string = "ui_selector"
|
||||
// assertions
|
||||
AssertionEqual string = "equal"
|
||||
AssertionNotEqual string = "not_equal"
|
||||
AssertionExists string = "exists"
|
||||
AssertionNotExists string = "not_exists"
|
||||
AssertionAI string = "ai_assert" // assert with ai
|
||||
)
|
||||
|
||||
type MobileAction struct {
|
||||
Method ActionMethod `json:"method,omitempty" yaml:"method,omitempty"`
|
||||
Method option.ActionMethod `json:"method,omitempty" yaml:"method,omitempty"`
|
||||
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||
Fn func() `json:"-" yaml:"-"` // used for function action, not serialized
|
||||
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
@@ -102,6 +32,7 @@ func (ma MobileAction) GetOptions() []option.ActionOption {
|
||||
return actionOptionList
|
||||
}
|
||||
|
||||
// TODO: merge to uixt MCP Server
|
||||
func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
actionStartTime := time.Now()
|
||||
defer func() {
|
||||
@@ -119,7 +50,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
}()
|
||||
|
||||
switch action.Method {
|
||||
case ACTION_WebLoginNoneUI:
|
||||
case option.ACTION_WebLoginNoneUI:
|
||||
if len(action.Params.([]interface{})) == 4 {
|
||||
driver, ok := dExt.IDriver.(*BrowserDriver)
|
||||
if !ok {
|
||||
@@ -129,53 +60,53 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
_, err = driver.LoginNoneUI(params[0].(string), params[1].(string), params[2].(string), params[3].(string))
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_WebLoginNoneUI, action.Params)
|
||||
case ACTION_AppInstall:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_WebLoginNoneUI, action.Params)
|
||||
case option.ACTION_AppInstall:
|
||||
if app, ok := action.Params.(string); ok {
|
||||
if err = dExt.GetDevice().Install(app,
|
||||
option.WithRetryTimes(action.MaxRetryTimes)); err != nil {
|
||||
return errors.Wrap(err, "failed to install app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppUninstall:
|
||||
case option.ACTION_AppUninstall:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.GetDevice().Uninstall(packageName); err != nil {
|
||||
return errors.Wrap(err, "failed to uninstall app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppClear:
|
||||
case option.ACTION_AppClear:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.AppClear(packageName); err != nil {
|
||||
return errors.Wrap(err, "failed to clear app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppLaunch:
|
||||
case option.ACTION_AppLaunch:
|
||||
if bundleId, ok := action.Params.(string); ok {
|
||||
return dExt.AppLaunch(bundleId)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
|
||||
ACTION_AppLaunch, action.Params)
|
||||
case ACTION_SwipeToTapApp:
|
||||
option.ACTION_AppLaunch, action.Params)
|
||||
case option.ACTION_SwipeToTapApp:
|
||||
if appName, ok := action.Params.(string); ok {
|
||||
return dExt.SwipeToTapApp(appName, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
|
||||
ACTION_SwipeToTapApp, action.Params)
|
||||
case ACTION_SwipeToTapText:
|
||||
option.ACTION_SwipeToTapApp, action.Params)
|
||||
case option.ACTION_SwipeToTapText:
|
||||
if text, ok := action.Params.(string); ok {
|
||||
return dExt.SwipeToTapTexts([]string{text}, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
|
||||
ACTION_SwipeToTapText, action.Params)
|
||||
case ACTION_SwipeToTapTexts:
|
||||
option.ACTION_SwipeToTapText, action.Params)
|
||||
case option.ACTION_SwipeToTapTexts:
|
||||
if texts, ok := action.Params.([]string); ok {
|
||||
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
|
||||
}
|
||||
if texts, err := builtin.ConvertToStringSlice(action.Params); err == nil {
|
||||
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_SwipeToTapTexts, action.Params)
|
||||
case ACTION_AppTerminate:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SwipeToTapTexts, action.Params)
|
||||
case option.ACTION_AppTerminate:
|
||||
if bundleId, ok := action.Params.(string); ok {
|
||||
success, err := dExt.AppTerminate(bundleId)
|
||||
if err != nil {
|
||||
@@ -187,9 +118,9 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
|
||||
case ACTION_Home:
|
||||
case option.ACTION_Home:
|
||||
return dExt.Home()
|
||||
case ACTION_SecondaryClick:
|
||||
case option.ACTION_SecondaryClick:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
if len(params) != 2 {
|
||||
return fmt.Errorf("invalid tap location params: %v", params)
|
||||
@@ -197,23 +128,23 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
x, y := params[0], params[1]
|
||||
return dExt.SecondaryClick(x, y)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_SecondaryClick, action.Params)
|
||||
case ACTION_HoverBySelector:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClick, action.Params)
|
||||
case option.ACTION_HoverBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.HoverBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_HoverBySelector, action.Params)
|
||||
case ACTION_TapBySelector:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_HoverBySelector, action.Params)
|
||||
case option.ACTION_TapBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.TapBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_TapBySelector, action.Params)
|
||||
case ACTION_SecondaryClickBySelector:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapBySelector, action.Params)
|
||||
case option.ACTION_SecondaryClickBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.SecondaryClickBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_SecondaryClickBySelector, action.Params)
|
||||
case ACTION_WebCloseTab:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClickBySelector, action.Params)
|
||||
case option.ACTION_WebCloseTab:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
paramInt64, _ := param.Int64()
|
||||
return dExt.IDriver.(*BrowserDriver).CloseTab(int(paramInt64))
|
||||
@@ -223,7 +154,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
return dExt.IDriver.(*BrowserDriver).CloseTab(action.Params.(int))
|
||||
}
|
||||
// return fmt.Errorf("invalid %s params: %v", ACTION_WebCloseTab, action.Params)
|
||||
case ACTION_SetIme:
|
||||
case option.ACTION_SetIme:
|
||||
if ime, ok := action.Params.(string); ok {
|
||||
err = dExt.SetIme(ime)
|
||||
if err != nil {
|
||||
@@ -231,7 +162,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_GetSource:
|
||||
case option.ACTION_GetSource:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
_, err = dExt.Source(option.WithProcessName(packageName))
|
||||
if err != nil {
|
||||
@@ -239,7 +170,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_TapXY:
|
||||
case option.ACTION_TapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
if len(params) != 2 {
|
||||
@@ -248,8 +179,8 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
x, y := params[0], params[1]
|
||||
return dExt.TapXY(x, y, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params)
|
||||
case ACTION_TapAbsXY:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapXY, action.Params)
|
||||
case option.ACTION_TapAbsXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// absolute coordinates x,y of window size: [100, 300]
|
||||
if len(params) != 2 {
|
||||
@@ -258,19 +189,19 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
x, y := params[0], params[1]
|
||||
return dExt.TapAbsXY(x, y, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params)
|
||||
case ACTION_TapByOCR:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapAbsXY, action.Params)
|
||||
case option.ACTION_TapByOCR:
|
||||
if ocrText, ok := action.Params.(string); ok {
|
||||
return dExt.TapByOCR(ocrText, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params)
|
||||
case ACTION_TapByCV:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByOCR, action.Params)
|
||||
case option.ACTION_TapByCV:
|
||||
actionOptions := option.NewActionOptions(action.GetOptions()...)
|
||||
if len(actionOptions.ScreenShotWithUITypes) > 0 {
|
||||
return dExt.TapByCV(action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params)
|
||||
case ACTION_DoubleTapXY:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByCV, action.Params)
|
||||
case option.ACTION_DoubleTapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
if len(params) != 2 {
|
||||
@@ -279,20 +210,20 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
x, y := params[0], params[1]
|
||||
return dExt.DoubleTap(x, y)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params)
|
||||
case ACTION_Swipe:
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_DoubleTapXY, action.Params)
|
||||
case option.ACTION_Swipe:
|
||||
params := action.Params
|
||||
swipeAction := prepareSwipeAction(dExt, params, action.GetOptions()...)
|
||||
return swipeAction(dExt)
|
||||
case ACTION_Input:
|
||||
case option.ACTION_Input:
|
||||
// input text on current active element
|
||||
// append \n to send text with enter
|
||||
// send \b\b\b to delete 3 chars
|
||||
param := fmt.Sprintf("%v", action.Params)
|
||||
return dExt.Input(param)
|
||||
case ACTION_Back:
|
||||
case option.ACTION_Back:
|
||||
return dExt.Back()
|
||||
case ACTION_Sleep:
|
||||
case option.ACTION_Sleep:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
seconds, _ := param.Float64()
|
||||
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
|
||||
@@ -315,7 +246,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
|
||||
case ACTION_SleepMS:
|
||||
case option.ACTION_SleepMS:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ := param.Int64()
|
||||
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
||||
@@ -328,29 +259,29 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep ms params: %v(%T)", action.Params, action.Params)
|
||||
case ACTION_SleepRandom:
|
||||
case option.ACTION_SleepRandom:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
sleepStrict(time.Now(), getSimulationDuration(params))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
|
||||
case ACTION_ScreenShot:
|
||||
case option.ACTION_ScreenShot:
|
||||
// take screenshot
|
||||
log.Info().Msg("take screenshot for current screen")
|
||||
_, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
|
||||
return err
|
||||
case ACTION_ClosePopups:
|
||||
case option.ACTION_ClosePopups:
|
||||
return dExt.ClosePopupsHandler()
|
||||
case ACTION_CallFunction:
|
||||
case option.ACTION_CallFunction:
|
||||
if funcDesc, ok := action.Params.(string); ok {
|
||||
return dExt.Call(funcDesc, action.Fn, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid function description: %v", action.Params)
|
||||
case ACTION_AIAction:
|
||||
case option.ACTION_AIAction:
|
||||
if prompt, ok := action.Params.(string); ok {
|
||||
return dExt.AIAction(prompt, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", ACTION_AIAction, action.Params)
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_AIAction, action.Params)
|
||||
default:
|
||||
log.Warn().Str("action", string(action.Method)).Msg("action not implemented")
|
||||
return errors.Wrapf(code.InvalidCaseError,
|
||||
|
||||
@@ -322,7 +322,7 @@ func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error
|
||||
}
|
||||
|
||||
// MarkUIOperation add operation mark for UI operation
|
||||
func MarkUIOperation(driver IDriver, actionType ActionMethod, actionCoordinates []float64) error {
|
||||
func MarkUIOperation(driver IDriver, actionType option.ActionMethod, actionCoordinates []float64) error {
|
||||
if actionType == "" || len(actionCoordinates) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -341,14 +341,14 @@ func MarkUIOperation(driver IDriver, actionType ActionMethod, actionCoordinates
|
||||
fmt.Sprintf("action_%s_pre_%s.png", timestamp, actionType),
|
||||
)
|
||||
|
||||
if actionType == ACTION_TapAbsXY || actionType == ACTION_DoubleTapXY {
|
||||
if actionType == option.ACTION_TapAbsXY || actionType == option.ACTION_DoubleTapXY {
|
||||
if len(actionCoordinates) != 2 {
|
||||
return fmt.Errorf("invalid tap action coordinates: %v", actionCoordinates)
|
||||
}
|
||||
x, y := actionCoordinates[0], actionCoordinates[1]
|
||||
point := image.Point{X: int(x), Y: int(y)}
|
||||
err = SaveImageWithCircleMarker(compressedBufSource, point, imagePath)
|
||||
} else if actionType == ACTION_Swipe || actionType == ACTION_Drag {
|
||||
} else if actionType == option.ACTION_Swipe || actionType == option.ACTION_Drag {
|
||||
if len(actionCoordinates) != 4 {
|
||||
return fmt.Errorf("invalid swipe action coordinates: %v", actionCoordinates)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, ra
|
||||
|
||||
// mark UI operation
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_TapAbsXY, []float64{x, y}); markErr != nil {
|
||||
if markErr := MarkUIOperation(driver, option.ACTION_TapAbsXY, []float64{x, y}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark tap operation")
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, r
|
||||
|
||||
// mark UI operation
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_DoubleTapXY, []float64{x, y}); markErr != nil {
|
||||
if markErr := MarkUIOperation(driver, option.ACTION_DoubleTapXY, []float64{x, y}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark double tap operation")
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw
|
||||
|
||||
// mark UI operation
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_Drag, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
if markErr := MarkUIOperation(driver, option.ACTION_Drag, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark drag operation")
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra
|
||||
|
||||
// save screenshot before action and mark UI operation
|
||||
if options.PreMarkOperation {
|
||||
if markErr := MarkUIOperation(driver, ACTION_Swipe, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
if markErr := MarkUIOperation(driver, option.ACTION_Swipe, []float64{fromX, fromY, toX, toY}); markErr != nil {
|
||||
log.Warn().Err(markErr).Msg("Failed to mark swipe operation")
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func preHandler_Swipe(driver IDriver, options *option.ActionOptions, rawFomX, ra
|
||||
return fromX, fromY, toX, toY, nil
|
||||
}
|
||||
|
||||
func postHandler(driver IDriver, actionType ActionMethod, options *option.ActionOptions) error {
|
||||
func postHandler(driver IDriver, actionType option.ActionMethod, options *option.ActionOptions) error {
|
||||
// save screenshot after action
|
||||
if options.PostMarkOperation {
|
||||
// get compressed screenshot buffer
|
||||
|
||||
@@ -130,23 +130,23 @@ func (dExt *XTDriver) assertOCR(text, assert string) error {
|
||||
opts = append(opts, option.WithScreenShotFileName(fmt.Sprintf("assert_ocr_%s", text)))
|
||||
|
||||
switch assert {
|
||||
case AssertionEqual:
|
||||
case option.AssertionEqual:
|
||||
_, err := dExt.FindScreenText(text, opts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "assert ocr equal failed")
|
||||
}
|
||||
case AssertionNotEqual:
|
||||
case option.AssertionNotEqual:
|
||||
_, err := dExt.FindScreenText(text, opts...)
|
||||
if err == nil {
|
||||
return errors.New("assert ocr not equal failed")
|
||||
}
|
||||
case AssertionExists:
|
||||
case option.AssertionExists:
|
||||
opts = append(opts, option.WithRegex(true))
|
||||
_, err := dExt.FindScreenText(text, opts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "assert ocr exists failed")
|
||||
}
|
||||
case AssertionNotExists:
|
||||
case option.AssertionNotExists:
|
||||
opts = append(opts, option.WithRegex(true))
|
||||
_, err := dExt.FindScreenText(text, opts...)
|
||||
if err == nil {
|
||||
@@ -166,11 +166,11 @@ func (dExt *XTDriver) assertForegroundApp(appName, assert string) error {
|
||||
}
|
||||
|
||||
switch assert {
|
||||
case AssertionEqual:
|
||||
case option.AssertionEqual:
|
||||
if app.PackageName != appName {
|
||||
return errors.Wrap(err, "assert foreground app equal failed")
|
||||
}
|
||||
case AssertionNotEqual:
|
||||
case option.AssertionNotEqual:
|
||||
if app.PackageName == appName {
|
||||
return errors.New("assert foreground app not equal failed")
|
||||
}
|
||||
@@ -186,12 +186,12 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error {
|
||||
return errors.New("assert selector only supports browser driver")
|
||||
}
|
||||
switch assert {
|
||||
case AssertionExists:
|
||||
case option.AssertionExists:
|
||||
_, err := driver.IsElementExistBySelector(selector)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "assert ocr exists failed")
|
||||
}
|
||||
case AssertionNotExists:
|
||||
case option.AssertionNotExists:
|
||||
_, err := driver.IsElementExistBySelector(selector)
|
||||
if err == nil {
|
||||
return errors.New("assert ocr not exists failed")
|
||||
@@ -204,13 +204,13 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error {
|
||||
|
||||
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) {
|
||||
switch check {
|
||||
case SelectorOCR:
|
||||
case option.SelectorOCR:
|
||||
err = dExt.assertOCR(expected, assert)
|
||||
case SelectorAI:
|
||||
case option.SelectorAI:
|
||||
err = dExt.AIAssert(assert)
|
||||
case SelectorForegroundApp:
|
||||
case option.SelectorForegroundApp:
|
||||
err = dExt.assertForegroundApp(expected, assert)
|
||||
case SelectorSelector:
|
||||
case option.SelectorSelector:
|
||||
err = dExt.assertSelector(expected, assert)
|
||||
default:
|
||||
return fmt.Errorf("validator %s not implemented", check)
|
||||
|
||||
@@ -159,7 +159,7 @@ func (hd *HDCDriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(hd, ACTION_TapAbsXY, actionOptions)
|
||||
defer postHandler(hd, option.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, ACTION_Swipe, actionOptions)
|
||||
defer postHandler(hd, option.ACTION_Swipe, actionOptions)
|
||||
|
||||
duration := 200
|
||||
if actionOptions.PressDuration > 0 {
|
||||
|
||||
@@ -602,7 +602,7 @@ func (wd *WDADriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer postHandler(wd, ACTION_TapAbsXY, actionOptions)
|
||||
defer postHandler(wd, option.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, ACTION_DoubleTapXY, actionOptions)
|
||||
defer postHandler(wd, option.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, ACTION_Drag, actionOptions)
|
||||
defer postHandler(wd, option.ACTION_Drag, actionOptions)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"fromX": math.Round(fromX*10) / 10,
|
||||
|
||||
@@ -8,6 +8,76 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ActionMethod string
|
||||
|
||||
const (
|
||||
ACTION_LOG ActionMethod = "log"
|
||||
ACTION_AppInstall ActionMethod = "install"
|
||||
ACTION_AppUninstall ActionMethod = "uninstall"
|
||||
ACTION_WebLoginNoneUI ActionMethod = "login_none_ui"
|
||||
ACTION_AppClear ActionMethod = "app_clear"
|
||||
ACTION_AppStart ActionMethod = "app_start"
|
||||
ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
|
||||
ACTION_AppTerminate ActionMethod = "app_terminate"
|
||||
ACTION_AppStop ActionMethod = "app_stop"
|
||||
ACTION_ScreenShot ActionMethod = "screenshot"
|
||||
ACTION_Sleep ActionMethod = "sleep"
|
||||
ACTION_SleepMS ActionMethod = "sleep_ms"
|
||||
ACTION_SleepRandom ActionMethod = "sleep_random"
|
||||
ACTION_SetIme ActionMethod = "set_ime"
|
||||
ACTION_GetSource ActionMethod = "get_source"
|
||||
ACTION_GetForegroundApp ActionMethod = "get_foreground_app"
|
||||
ACTION_CallFunction ActionMethod = "call_function"
|
||||
|
||||
// UI handling
|
||||
ACTION_Home ActionMethod = "home"
|
||||
ACTION_TapXY ActionMethod = "tap_xy"
|
||||
ACTION_TapAbsXY ActionMethod = "tap_abs_xy"
|
||||
ACTION_TapByOCR ActionMethod = "tap_ocr"
|
||||
ACTION_TapByCV ActionMethod = "tap_cv"
|
||||
ACTION_DoubleTapXY ActionMethod = "double_tap_xy"
|
||||
ACTION_Swipe ActionMethod = "swipe"
|
||||
ACTION_Drag ActionMethod = "drag"
|
||||
ACTION_Input ActionMethod = "input"
|
||||
ACTION_Back ActionMethod = "back"
|
||||
ACTION_KeyCode ActionMethod = "keycode"
|
||||
ACTION_AIAction ActionMethod = "ai_action" // action with ai
|
||||
ACTION_TapBySelector ActionMethod = "tap_by_selector"
|
||||
ACTION_HoverBySelector ActionMethod = "hover_by_selector"
|
||||
ACTION_WebCloseTab ActionMethod = "web_close_tab"
|
||||
ACTION_SecondaryClick ActionMethod = "secondary_click"
|
||||
ACTION_SecondaryClickBySelector ActionMethod = "secondary_click_by_selector"
|
||||
ACTION_GetElementTextBySelector ActionMethod = "get_element_text_by_selector"
|
||||
|
||||
// custom actions
|
||||
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
|
||||
ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
|
||||
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
||||
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
||||
ACTION_InstallApp ActionMethod = "install_app"
|
||||
ACTION_UninstallApp ActionMethod = "uninstall_app"
|
||||
ACTION_DownloadApp ActionMethod = "download_app"
|
||||
)
|
||||
|
||||
const (
|
||||
// UI validation
|
||||
// selectors
|
||||
SelectorName string = "ui_name"
|
||||
SelectorLabel string = "ui_label"
|
||||
SelectorOCR string = "ui_ocr"
|
||||
SelectorImage string = "ui_image"
|
||||
SelectorAI string = "ui_ai" // ui query with ai
|
||||
SelectorForegroundApp string = "ui_foreground_app"
|
||||
SelectorSelector string = "ui_selector"
|
||||
// assertions
|
||||
AssertionEqual string = "equal"
|
||||
AssertionNotEqual string = "not_equal"
|
||||
AssertionExists string = "exists"
|
||||
AssertionNotExists string = "not_exists"
|
||||
AssertionAI string = "ai_assert" // assert with ai
|
||||
)
|
||||
|
||||
type ActionOptions struct {
|
||||
Context context.Context `json:"-" yaml:"-"`
|
||||
// log
|
||||
|
||||
@@ -94,7 +94,7 @@ func convertActionToCallToolRequest(action MobileAction) (mcp.CallToolRequest, e
|
||||
return mcp.CallToolRequest{}, nil
|
||||
}
|
||||
|
||||
func (dExt *XTDriver) DoAction2(action MobileAction) (err error) {
|
||||
func (dExt *XTDriver) ExecuteAction(action MobileAction) (err error) {
|
||||
// convert action to call tool request
|
||||
req, err := convertActionToCallToolRequest(action)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user