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
+6 -4
View File
@@ -43,8 +43,8 @@ type TConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
MCPConfigPath string `json:"mcp_config_path,omitempty" yaml:"mcp_config_path,omitempty"` 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 AntiRisk bool `json:"anti_risk,omitempty" yaml:"anti_risk,omitempty"` // global anti-risk switch
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` 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"` AIOptions *option.AIServiceOptions `json:"ai_options,omitempty" yaml:"ai_options,omitempty"`
} }
@@ -230,8 +230,10 @@ func (c *TConfig) EnablePlugin() *TConfig {
return c return c
} }
func (c *TConfig) DisableAutoPopupHandler() *TConfig { // EnableAutoPopupHandler enables auto popup handler for current testcase.
c.IgnorePopup = true // default to disable auto popup handler
func (c *TConfig) EnableAutoPopupHandler() *TConfig {
c.AutoPopupHandler = true
return c return c
} }
+9 -9
View File
@@ -220,15 +220,15 @@ v5 版本增加了完善的超时和中断处理机制:
步骤配置支持更多选项: 步骤配置支持更多选项:
```go ```go
type StepConfig struct { type StepConfig struct {
StepName string `json:"name" yaml:"name"` // required StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"` Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"` StepExport []string `json:"export,omitempty" yaml:"export,omitempty"`
Loops *types.IntOrString `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"` AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
} }
``` ```
+1 -1
View File
@@ -1 +1 @@
v5.0.0-beta-2506132024 v5.0.0-beta-2506141211
+14 -40
View File
@@ -11,7 +11,6 @@ import (
"os" "os"
"os/signal" "os/signal"
"reflect" "reflect"
"strconv"
"strings" "strings"
"syscall" "syscall"
"testing" "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") log.Info().Str("step", stepName).Str("type", stepType).Msg("run step start")
// run times of step // run times of step
loopTimes, err := r.getLoopTimes(step) loopTimes := step.Config().Loops
if err != nil { if loopTimes == 0 {
return nil, errors.Wrap(err, "failed to get loop times") 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 // run step with specified loop times
@@ -804,6 +805,15 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary {
return r.summary 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 { func (r *SessionRunner) ParseStep(step IStep) error {
caseConfig := r.caseRunner.TestCase.Config.Get() caseConfig := r.caseRunner.TestCase.Config.Get()
stepConfig := step.Config() stepConfig := step.Config()
@@ -880,39 +890,3 @@ func (r *SessionRunner) GetSessionVariables() map[string]interface{} {
func (r *SessionRunner) GetTransactions() map[string]map[TransactionType]time.Time { func (r *SessionRunner) GetTransactions() map[string]map[TransactionType]time.Time {
return r.transactions 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
}
+9 -10
View File
@@ -3,7 +3,6 @@ package hrp
import ( import (
"github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
) )
type StepType string type StepType string
@@ -28,15 +27,15 @@ const (
) )
type StepConfig struct { type StepConfig struct {
StepName string `json:"name" yaml:"name"` // required StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"` Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"` StepExport []string `json:"export,omitempty" yaml:"export,omitempty"`
Loops *types.IntOrString `json:"loops,omitempty" yaml:"loops,omitempty"` Loops int `json:"loops,omitempty" yaml:"loops,omitempty"`
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
} }
// define struct for teststep // define struct for teststep
+1 -4
View File
@@ -25,7 +25,6 @@ import (
"github.com/httprunner/httprunner/v5/internal/httpstat" "github.com/httprunner/httprunner/v5/internal/httpstat"
"github.com/httprunner/httprunner/v5/internal/json" "github.com/httprunner/httprunner/v5/internal/json"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
) )
type HTTPMethod string type HTTPMethod string
@@ -560,9 +559,7 @@ func (s *StepRequest) HTTP2() *StepRequest {
// Loop specify running times for the current step // Loop specify running times for the current step
func (s *StepRequest) Loop(times int) *StepRequest { func (s *StepRequest) Loop(times int) *StepRequest {
s.Loops = &types.IntOrString{ s.Loops = times
IntValue: &times,
}
return s return s
} }
+21 -10
View File
@@ -458,11 +458,6 @@ func (s *StepMobile) ScreenShot(opts ...option.ActionOption) *StepMobile {
return s return s
} }
func (s *StepMobile) DisableAutoPopupHandler() *StepMobile {
s.IgnorePopup = true
return s
}
func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile { func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
s.obj().Actions = append(s.obj().Actions, option.MobileAction{ s.obj().Actions = append(s.obj().Actions, option.MobileAction{
Method: option.ACTION_ClosePopups, Method: option.ACTION_ClosePopups,
@@ -472,6 +467,12 @@ func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
return s 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 { func (s *StepMobile) Call(name string, fn func(), opts ...option.ActionOption) *StepMobile {
s.obj().Actions = append(s.obj().Actions, option.MobileAction{ s.obj().Actions = append(s.obj().Actions, option.MobileAction{
Method: option.ACTION_CallFunction, Method: option.ACTION_CallFunction,
@@ -713,19 +714,19 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
var stepVariables map[string]interface{} var stepVariables map[string]interface{}
var stepValidators []interface{} var stepValidators []interface{}
var ignorePopup bool var stepAutoPopupHandler bool
var mobileStep *MobileUI var mobileStep *MobileUI
switch stepMobile := step.(type) { switch stepMobile := step.(type) {
case *StepMobile: case *StepMobile:
mobileStep = stepMobile.obj() mobileStep = stepMobile.obj()
stepVariables = stepMobile.Variables stepVariables = stepMobile.Variables
ignorePopup = stepMobile.IgnorePopup stepAutoPopupHandler = stepMobile.AutoPopupHandler
case *StepMobileUIValidation: case *StepMobileUIValidation:
mobileStep = stepMobile.obj() mobileStep = stepMobile.obj()
stepVariables = stepMobile.Variables stepVariables = stepMobile.Variables
stepValidators = stepMobile.Validators stepValidators = stepMobile.Validators
ignorePopup = stepMobile.StepMobile.IgnorePopup stepAutoPopupHandler = stepMobile.StepMobile.AutoPopupHandler
default: default:
return stepResult, errors.New("invalid mobile UI step type") 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) stepResult.Actions = append(stepResult.Actions, actionResult)
} }
// automatic handling of pop-up windows on each step finished
var config *TConfig var config *TConfig
if s.caseRunner != nil && s.caseRunner.Config != nil { if s.caseRunner != nil && s.caseRunner.Config != nil {
config = s.caseRunner.Config.Get() 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() startTime := time.Now()
actionResult := &ActionResult{ actionResult := &ActionResult{
MobileAction: option.MobileAction{ MobileAction: option.MobileAction{