feat: implement two-level auto popup handler configuration

- Add AutoPopupHandler field to both TConfig and StepConfig
- Support testcase-level global configuration via TConfig.EnableAutoPopupHandler()
- Support step-level specific configuration via StepMobile.EnableAutoPopupHandler()
- Priority: testcase config > step config > default disabled
- Simplify Loops field type from *types.IntOrString to int in StepConfig
- Update documentation to reflect new structure
This commit is contained in:
lilong.129
2025-06-14 12:11:04 +08:00
parent b271e655b1
commit 1145f424b1
7 changed files with 61 additions and 78 deletions

View File

@@ -43,8 +43,8 @@ type TConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
MCPConfigPath string `json:"mcp_config_path,omitempty" yaml:"mcp_config_path,omitempty"`
AntiRisk bool `json:"anti_risk,omitempty" yaml:"anti_risk,omitempty"` // global anti-risk switch
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
AntiRisk bool `json:"anti_risk,omitempty" yaml:"anti_risk,omitempty"` // global anti-risk switch
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler
AIOptions *option.AIServiceOptions `json:"ai_options,omitempty" yaml:"ai_options,omitempty"`
}
@@ -230,8 +230,10 @@ func (c *TConfig) EnablePlugin() *TConfig {
return c
}
func (c *TConfig) DisableAutoPopupHandler() *TConfig {
c.IgnorePopup = true
// EnableAutoPopupHandler enables auto popup handler for current testcase.
// default to disable auto popup handler
func (c *TConfig) EnableAutoPopupHandler() *TConfig {
c.AutoPopupHandler = true
return c
}

View File

@@ -220,15 +220,15 @@ v5 版本增加了完善的超时和中断处理机制:
步骤配置支持更多选项:
```go
type StepConfig struct {
StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
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 *types.IntOrString `json:"loops,omitempty" yaml:"loops,omitempty"`
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
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 *types.IntOrString `json:"loops,omitempty" yaml:"loops,omitempty"`
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
}
```

View File

@@ -1 +1 @@
v5.0.0-beta-2506132024
v5.0.0-beta-2506141211

View File

@@ -11,7 +11,6 @@ import (
"os"
"os/signal"
"reflect"
"strconv"
"strings"
"syscall"
"testing"
@@ -752,9 +751,11 @@ 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, err := r.getLoopTimes(step)
if err != nil {
return nil, errors.Wrap(err, "failed to get loop times")
loopTimes := step.Config().Loops
if loopTimes == 0 {
loopTimes = 1 // default run once
} else if loopTimes > 1 {
log.Info().Int("loops", loopTimes).Msg("set multiple loop times")
}
// run step with specified loop times
@@ -804,6 +805,15 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary {
return r.summary
}
// GenerateReport generates report for the testcase.
func (r *SessionRunner) GenerateReport() error {
summary := NewSummary()
caseSummary := r.GetSummary()
summary.AddCaseSummary(caseSummary)
summary.Time.Duration = time.Since(caseSummary.Time.StartAt).Seconds()
return summary.GenHTMLReport()
}
func (r *SessionRunner) ParseStep(step IStep) error {
caseConfig := r.caseRunner.TestCase.Config.Get()
stepConfig := step.Config()
@@ -880,39 +890,3 @@ 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
}

19
step.go
View File

@@ -3,7 +3,6 @@ package hrp
import (
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
)
type StepType string
@@ -28,15 +27,15 @@ const (
)
type StepConfig struct {
StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
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 *types.IntOrString `json:"loops,omitempty" yaml:"loops,omitempty"`
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
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"`
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
}
// define struct for teststep

View File

@@ -25,7 +25,6 @@ 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
@@ -560,9 +559,7 @@ func (s *StepRequest) HTTP2() *StepRequest {
// Loop specify running times for the current step
func (s *StepRequest) Loop(times int) *StepRequest {
s.Loops = &types.IntOrString{
IntValue: &times,
}
s.Loops = times
return s
}

View File

@@ -458,11 +458,6 @@ func (s *StepMobile) ScreenShot(opts ...option.ActionOption) *StepMobile {
return s
}
func (s *StepMobile) DisableAutoPopupHandler() *StepMobile {
s.IgnorePopup = true
return s
}
func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
s.obj().Actions = append(s.obj().Actions, option.MobileAction{
Method: option.ACTION_ClosePopups,
@@ -472,6 +467,12 @@ func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
return s
}
// EnableAutoPopupHandler enables auto popup handler for this step.
func (s *StepMobile) EnableAutoPopupHandler() *StepMobile {
s.AutoPopupHandler = true
return s
}
func (s *StepMobile) Call(name string, fn func(), opts ...option.ActionOption) *StepMobile {
s.obj().Actions = append(s.obj().Actions, option.MobileAction{
Method: option.ACTION_CallFunction,
@@ -713,19 +714,19 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
var stepVariables map[string]interface{}
var stepValidators []interface{}
var ignorePopup bool
var stepAutoPopupHandler bool
var mobileStep *MobileUI
switch stepMobile := step.(type) {
case *StepMobile:
mobileStep = stepMobile.obj()
stepVariables = stepMobile.Variables
ignorePopup = stepMobile.IgnorePopup
stepAutoPopupHandler = stepMobile.AutoPopupHandler
case *StepMobileUIValidation:
mobileStep = stepMobile.obj()
stepVariables = stepMobile.Variables
stepValidators = stepMobile.Validators
ignorePopup = stepMobile.StepMobile.IgnorePopup
stepAutoPopupHandler = stepMobile.StepMobile.AutoPopupHandler
default:
return stepResult, errors.New("invalid mobile UI step type")
}
@@ -792,12 +793,22 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
stepResult.Actions = append(stepResult.Actions, actionResult)
}
// automatic handling of pop-up windows on each step finished
var config *TConfig
if s.caseRunner != nil && s.caseRunner.Config != nil {
config = s.caseRunner.Config.Get()
}
if !ignorePopup && (config == nil || !config.IgnorePopup) && uiDriver != nil {
// automatic handling of pop-up windows on each step finished
// priority: testcase config > step config, default to disabled
shouldHandlePopup := false
if config != nil && config.AutoPopupHandler {
// testcase level config has higher priority
shouldHandlePopup = true
} else if stepAutoPopupHandler {
// step level config
shouldHandlePopup = true
}
if shouldHandlePopup && uiDriver != nil {
startTime := time.Now()
actionResult := &ActionResult{
MobileAction: option.MobileAction{