mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-30 04:49:33 +08:00
feat: save screenshot file
This commit is contained in:
@@ -2,20 +2,14 @@ package hrp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/electricbubble/gwda"
|
"github.com/electricbubble/gwda"
|
||||||
|
gwdaExt "github.com/electricbubble/gwda-ext-opencv"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -335,7 +329,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if WDA is healthy
|
// check if WDA is healthy
|
||||||
ok, e := client.Driver.IsWdaHealthy()
|
ok, e := client.DriverExt.IsWdaHealthy()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(e, "check WDA health failed")
|
err = errors.Wrap(e, "check WDA health failed")
|
||||||
return
|
return
|
||||||
@@ -391,7 +385,11 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||||
}
|
}
|
||||||
settings, err := driver.SetAppiumSettings(map[string]interface{}{
|
driverExt, err := gwdaExt.Extend(driver, 0.95)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to extend gwda.WebDriver")
|
||||||
|
}
|
||||||
|
settings, err := driverExt.SetAppiumSettings(map[string]interface{}{
|
||||||
"snapshotMaxDepth": snapshotMaxDepth,
|
"snapshotMaxDepth": snapshotMaxDepth,
|
||||||
"acceptAlertButtonSelector": acceptAlertButtonSelector,
|
"acceptAlertButtonSelector": acceptAlertButtonSelector,
|
||||||
})
|
})
|
||||||
@@ -401,7 +399,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro
|
|||||||
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
||||||
|
|
||||||
// get device window size
|
// get device window size
|
||||||
windowSize, err := driver.WindowSize()
|
windowSize, err := driverExt.WindowSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to get windows size")
|
return nil, errors.Wrap(err, "failed to get windows size")
|
||||||
}
|
}
|
||||||
@@ -411,7 +409,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro
|
|||||||
client = &wdaClient{
|
client = &wdaClient{
|
||||||
ID: time.Now().Unix(),
|
ID: time.Now().Unix(),
|
||||||
Device: targetDevice,
|
Device: targetDevice,
|
||||||
Driver: driver,
|
DriverExt: driverExt,
|
||||||
WindowSize: windowSize,
|
WindowSize: windowSize,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@@ -457,11 +455,11 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// take snapshot
|
// take snapshot
|
||||||
log.Info().Str("name", step.Name).Msg("take snapshot before validation")
|
screenshotPath, err := wdaClient.DriverExt.ScreenShot(fmt.Sprintf("validate_%s", step.Name))
|
||||||
err = wdaClient.screenShot(fmt.Sprintf("validate_%s", step.Name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed")
|
log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed")
|
||||||
}
|
}
|
||||||
|
log.Info().Str("path", screenshotPath).Msg("take screenshot before validation")
|
||||||
|
|
||||||
// validate
|
// validate
|
||||||
validateResults, err := wdaClient.doValidation(step.Validators)
|
validateResults, err := wdaClient.doValidation(step.Validators)
|
||||||
@@ -480,66 +478,11 @@ var errActionNotImplemented = errors.New("UI action not implemented")
|
|||||||
type wdaClient struct {
|
type wdaClient struct {
|
||||||
ID int64
|
ID int64
|
||||||
Device *gwda.Device
|
Device *gwda.Device
|
||||||
Driver gwda.WebDriver
|
DriverExt *gwdaExt.DriverExt
|
||||||
WindowSize gwda.Size
|
WindowSize gwda.Size
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenShot takes screenshot and saves image file to $CWD/screenshots/ folder
|
|
||||||
func (w *wdaClient) screenShot(path ...string) error {
|
|
||||||
// gidevice 和 gwda 均可实现截图功能,但 gidevice 的截图性能更优
|
|
||||||
// gwda 通过 wda 请求获取(分辨率、响应时间均由 wda 决定)
|
|
||||||
// gidevice 直接通过 Apple 允许的底层通信获取
|
|
||||||
// raw, err := w.Driver.Screenshot()
|
|
||||||
raw, err := w.Device.GIDevice().Screenshot()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "screenshot by WDA failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
img, format, err := image.Decode(raw)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "decode screenshot image failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, _ := os.Getwd()
|
|
||||||
screenshotsDir := filepath.Join(dir, "screenshots")
|
|
||||||
if err := builtin.EnsureFolderExists(screenshotsDir); err != nil {
|
|
||||||
return errors.Wrap(err, "create screenshots directory failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
var filaName string
|
|
||||||
if len(path) > 0 && path[0] != "" {
|
|
||||||
filaName = fmt.Sprintf("%d_%s", time.Now().Unix(), path[0])
|
|
||||||
} else {
|
|
||||||
filaName = fmt.Sprintf("%d", time.Now().Unix())
|
|
||||||
}
|
|
||||||
screenshotPath := filepath.Join(screenshotsDir,
|
|
||||||
fmt.Sprintf("%d_%s.%s", w.ID, filaName, format))
|
|
||||||
|
|
||||||
file, err := os.Create(screenshotPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "create screenshot image file failed")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = file.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
switch format {
|
|
||||||
case "png":
|
|
||||||
err = png.Encode(file, img)
|
|
||||||
case "jpeg":
|
|
||||||
err = jpeg.Encode(file, img, nil)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported image format: %s", format)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "encode screenshot image failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("path", screenshotPath).Msg("screenshot generated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wdaClient) doAction(action MobileAction) error {
|
func (w *wdaClient) doAction(action MobileAction) error {
|
||||||
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action")
|
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action")
|
||||||
|
|
||||||
@@ -549,17 +492,17 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
return errActionNotImplemented
|
return errActionNotImplemented
|
||||||
case appLaunch:
|
case appLaunch:
|
||||||
if bundleId, ok := action.Params.(string); ok {
|
if bundleId, ok := action.Params.(string); ok {
|
||||||
return w.Driver.AppLaunch(bundleId)
|
return w.DriverExt.AppLaunch(bundleId)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params)
|
return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params)
|
||||||
case appLaunchUnattached:
|
case appLaunchUnattached:
|
||||||
if bundleId, ok := action.Params.(string); ok {
|
if bundleId, ok := action.Params.(string); ok {
|
||||||
return w.Driver.AppLaunchUnattached(bundleId)
|
return w.DriverExt.AppLaunchUnattached(bundleId)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params)
|
return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params)
|
||||||
case appTerminate:
|
case appTerminate:
|
||||||
if bundleId, ok := action.Params.(string); ok {
|
if bundleId, ok := action.Params.(string); ok {
|
||||||
success, err := w.Driver.AppTerminate(bundleId)
|
success, err := w.DriverExt.AppTerminate(bundleId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to terminate app")
|
return errors.Wrap(err, "failed to terminate app")
|
||||||
}
|
}
|
||||||
@@ -570,7 +513,7 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
|
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
|
||||||
case uiHome:
|
case uiHome:
|
||||||
return w.Driver.Homescreen()
|
return w.DriverExt.Homescreen()
|
||||||
case uiClick:
|
case uiClick:
|
||||||
// click on coordinate
|
// click on coordinate
|
||||||
if location, ok := action.Params.([]int); ok {
|
if location, ok := action.Params.([]int); ok {
|
||||||
@@ -578,7 +521,7 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
if len(location) != 2 {
|
if len(location) != 2 {
|
||||||
return fmt.Errorf("invalid click location params: %v", location)
|
return fmt.Errorf("invalid click location params: %v", location)
|
||||||
}
|
}
|
||||||
return w.Driver.Tap(location[0], location[1])
|
return w.DriverExt.WebDriver.Tap(location[0], location[1])
|
||||||
}
|
}
|
||||||
if location, ok := action.Params.([]float64); ok {
|
if location, ok := action.Params.([]float64); ok {
|
||||||
// relative x,y of window size
|
// relative x,y of window size
|
||||||
@@ -587,7 +530,7 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
}
|
}
|
||||||
x := location[0] * float64(w.WindowSize.Width)
|
x := location[0] * float64(w.WindowSize.Width)
|
||||||
y := location[1] * float64(w.WindowSize.Height)
|
y := location[1] * float64(w.WindowSize.Height)
|
||||||
return w.Driver.TapFloat(x, y)
|
return w.DriverExt.TapFloat(x, y)
|
||||||
}
|
}
|
||||||
// click on name or xpath
|
// click on name or xpath
|
||||||
if param, ok := action.Params.(string); ok {
|
if param, ok := action.Params.(string); ok {
|
||||||
@@ -642,13 +585,13 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid swipe params: %v", action.Params)
|
return fmt.Errorf("invalid swipe params: %v", action.Params)
|
||||||
}
|
}
|
||||||
return w.Driver.Swipe(fromX, fromY, toX, toY)
|
return w.DriverExt.WebDriver.Swipe(fromX, fromY, toX, toY)
|
||||||
case uiInput:
|
case uiInput:
|
||||||
// input text on current active element
|
// input text on current active element
|
||||||
// append \n to send text with enter
|
// append \n to send text with enter
|
||||||
// send \b\b\b to delete 3 chars
|
// send \b\b\b to delete 3 chars
|
||||||
param := fmt.Sprintf("%v", action.Params)
|
param := fmt.Sprintf("%v", action.Params)
|
||||||
return w.Driver.SendKeys(param)
|
return w.DriverExt.SendKeys(param)
|
||||||
case ctlSleep:
|
case ctlSleep:
|
||||||
if param, ok := action.Params.(int); ok {
|
if param, ok := action.Params.(int); ok {
|
||||||
time.Sleep(time.Duration(param) * time.Second)
|
time.Sleep(time.Duration(param) * time.Second)
|
||||||
@@ -658,16 +601,21 @@ func (w *wdaClient) doAction(action MobileAction) error {
|
|||||||
case ctlScreenShot:
|
case ctlScreenShot:
|
||||||
// take snapshot
|
// take snapshot
|
||||||
log.Info().Msg("take snapshot for current screen")
|
log.Info().Msg("take snapshot for current screen")
|
||||||
|
var screenshotPath string
|
||||||
|
var err error
|
||||||
if param, ok := action.Params.(string); ok {
|
if param, ok := action.Params.(string); ok {
|
||||||
return w.screenShot(fmt.Sprintf("screenshot_%s", param))
|
screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%s", param))
|
||||||
|
} else {
|
||||||
|
screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix()))
|
||||||
}
|
}
|
||||||
return w.screenShot()
|
log.Info().Str("path", screenshotPath).Msg("take screenshot")
|
||||||
|
return err
|
||||||
case ctlStartCamera:
|
case ctlStartCamera:
|
||||||
// start camera, alias for app_launch com.apple.camera
|
// start camera, alias for app_launch com.apple.camera
|
||||||
return w.Driver.AppLaunch("com.apple.camera")
|
return w.DriverExt.AppLaunch("com.apple.camera")
|
||||||
case ctlStopCamera:
|
case ctlStopCamera:
|
||||||
// stop camera, alias for app_terminate com.apple.camera
|
// stop camera, alias for app_terminate com.apple.camera
|
||||||
success, err := w.Driver.AppTerminate("com.apple.camera")
|
success, err := w.DriverExt.AppTerminate("com.apple.camera")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to terminate camera")
|
return errors.Wrap(err, "failed to terminate camera")
|
||||||
}
|
}
|
||||||
@@ -752,28 +700,14 @@ func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Driver.FindElement(selector)
|
return w.DriverExt.FindElement(selector)
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wdaClient) locateByOCR(text string) (point gwda.Point, err error) {
|
|
||||||
// raw, err := w.Device.GIDevice().Screenshot()
|
|
||||||
// if err != nil {
|
|
||||||
// return errors.Wrap(err, "screenshot by WDA failed")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// url := "https://hubble.bytedance.net/video/api/v1/algorithm/ocr"
|
|
||||||
|
|
||||||
// req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode()))
|
|
||||||
|
|
||||||
// w.httpClient.Do(req)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *wdaClient) assertName(name string, exists bool) bool {
|
func (w *wdaClient) assertName(name string, exists bool) bool {
|
||||||
selector := gwda.BySelector{
|
selector := gwda.BySelector{
|
||||||
LinkText: gwda.NewElementAttribute().WithName(name),
|
LinkText: gwda.NewElementAttribute().WithName(name),
|
||||||
}
|
}
|
||||||
_, err := w.Driver.FindElement(selector)
|
_, err := w.DriverExt.FindElement(selector)
|
||||||
return exists == (err == nil)
|
return exists == (err == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +715,6 @@ func (w *wdaClient) assertXpath(xpath string, exists bool) bool {
|
|||||||
selector := gwda.BySelector{
|
selector := gwda.BySelector{
|
||||||
XPath: xpath,
|
XPath: xpath,
|
||||||
}
|
}
|
||||||
_, err := w.Driver.FindElement(selector)
|
_, err := w.DriverExt.FindElement(selector)
|
||||||
return exists == (err == nil)
|
return exists == (err == nil)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user