mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-08 08:57:40 +08:00
Merge branch 'v5-feat' into 'v5'
feat-android-screenrecord See merge request iesqa/httprunner!68
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2503052233
|
v5.0.0-beta-2503062216
|
||||||
|
|||||||
+59
-4
@@ -2,6 +2,7 @@ package gadb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -147,15 +148,28 @@ func (d *Device) Usb() (string, error) {
|
|||||||
return "", errors.New("does not have attribute: usb")
|
return "", errors.New("does not have attribute: usb")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Device) SystemVersion() (string, error) {
|
||||||
|
if d.HasAttribute("systemVersion") {
|
||||||
|
return d.attrs["systemVersion"], nil
|
||||||
|
}
|
||||||
|
systemVersion, err := d.RunShellCommand("getprop", "ro.build.version.release")
|
||||||
|
systemVersion = strings.TrimSpace(systemVersion)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("get android system version failed")
|
||||||
|
}
|
||||||
|
d.attrs["systemVersion"] = systemVersion
|
||||||
|
return systemVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Device) SdkVersion() (string, error) {
|
func (d *Device) SdkVersion() (string, error) {
|
||||||
if d.HasAttribute("sdkVersion") {
|
if d.HasAttribute("sdkVersion") {
|
||||||
return d.attrs["sdkVersion"], nil
|
return d.attrs["sdkVersion"], nil
|
||||||
}
|
}
|
||||||
sdkVersion, err := d.RunShellCommand("getprop", "ro.build.version.sdk")
|
sdkVersion, err := d.RunShellCommand("getprop", "ro.build.version.sdk")
|
||||||
sdkVersion = strings.TrimSpace(sdkVersion)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("does not have attribute: sdkVersion")
|
return "", errors.New("get android sdk version failed")
|
||||||
}
|
}
|
||||||
|
sdkVersion = strings.TrimSpace(sdkVersion)
|
||||||
d.attrs["sdkVersion"] = sdkVersion
|
d.attrs["sdkVersion"] = sdkVersion
|
||||||
return sdkVersion, nil
|
return sdkVersion, nil
|
||||||
}
|
}
|
||||||
@@ -732,7 +746,7 @@ func (d *Device) ScreenCap() ([]byte, error) {
|
|||||||
time.Now().Unix())
|
time.Now().Unix())
|
||||||
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
|
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "screencap failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove temp file
|
// remove temp file
|
||||||
@@ -742,5 +756,46 @@ func (d *Device) ScreenCap() ([]byte, error) {
|
|||||||
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
err = d.Pull(tempPath, buffer)
|
err = d.Pull(tempPath, buffer)
|
||||||
return buffer.Bytes(), err
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "pull video failed")
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) ScreenRecord(ctx context.Context) ([]byte, error) {
|
||||||
|
videoPath := fmt.Sprintf("/sdcard/screenrecord_%d.mp4", time.Now().Unix())
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := d.RunShellCommandWithBytes("screenrecord", videoPath)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// timeout or cancelled
|
||||||
|
pid, err := d.RunShellCommand("pidof", "screenrecord")
|
||||||
|
if err == nil && pid != "" {
|
||||||
|
// 发送 SIGINT 信号终止录屏
|
||||||
|
_, _ = d.RunShellCommand("kill", "-2", strings.TrimSpace(pid))
|
||||||
|
}
|
||||||
|
<-done // 等待进程完全退出
|
||||||
|
case err := <-done:
|
||||||
|
// adb screenrecord will exit on reached 180s
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "screenrecord failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove temp file
|
||||||
|
defer func() {
|
||||||
|
go d.RunShellCommand("rm", videoPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
err := d.Pull(videoPath, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "pull video failed")
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-3
@@ -4,11 +4,15 @@ package gadb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var devices []*Device
|
var devices []*Device
|
||||||
@@ -17,9 +21,7 @@ func setupDevices(t *testing.T) {
|
|||||||
var err error
|
var err error
|
||||||
setupClient(t)
|
setupClient(t)
|
||||||
devices, err = adbClient.DeviceList()
|
devices, err = adbClient.DeviceList()
|
||||||
if err != nil {
|
require.Nil(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDevice_State(t *testing.T) {
|
func TestDevice_State(t *testing.T) {
|
||||||
@@ -119,6 +121,24 @@ func TestDevice_DeviceInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevice_SdkVersion(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
for _, device := range devices {
|
||||||
|
sdkVersion, err := device.SdkVersion()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
t.Log(device.Serial(), sdkVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevice_SystemVersion(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
for _, device := range devices {
|
||||||
|
systemVersion, err := device.SystemVersion()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
t.Log(device.Serial(), systemVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDevice_Forward(t *testing.T) {
|
func TestDevice_Forward(t *testing.T) {
|
||||||
setupDevices(t)
|
setupDevices(t)
|
||||||
|
|
||||||
@@ -276,6 +296,36 @@ func TestDevice_Pull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevice_ScreenRecord(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
|
||||||
|
for _, dev := range devices {
|
||||||
|
// screen record with time limit 5 seconds
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
if _, err := dev.ScreenRecord(ctx); err != nil {
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range devices {
|
||||||
|
// screen record with cancel signal
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
_, err := dev.ScreenRecord(ctx)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// record for 3 seconds
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := <-done
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
|
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
adbClient Client
|
adbClient Client
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ func (r *Router) screenResultHandler(c *gin.Context) {
|
|||||||
|
|
||||||
var actionOptions []option.ActionOption
|
var actionOptions []option.ActionOption
|
||||||
if screenReq.Options != nil {
|
if screenReq.Options != nil {
|
||||||
actionOptions = screenReq.Options.Options()
|
actionOptions = screenReq.Options.GetScreenShotOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
screenResult, err := driver.GetScreenResult(actionOptions...)
|
screenResult, err := driver.GetScreenResult(actionOptions...)
|
||||||
|
|||||||
+98
-24
@@ -3,6 +3,7 @@ package uixt
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -779,54 +780,127 @@ func (ad *ADBDriver) GetIme() (ime string, err error) {
|
|||||||
return currentIme, nil
|
return currentIme, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *ADBDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||||
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
options := option.NewActionOptions(opts...)
|
||||||
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
|
||||||
|
|
||||||
file, err := os.Create(fileName)
|
var filePath string
|
||||||
if err != nil {
|
if options.ScreenRecordPath != "" {
|
||||||
log.Error().Err(err)
|
filePath = options.ScreenRecordPath
|
||||||
return "", err
|
} else {
|
||||||
|
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
||||||
|
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
if options.Context != nil {
|
||||||
|
ctx = options.Context
|
||||||
|
} else {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
duration := options.ScreenRecordDuration
|
||||||
|
if duration == 0 {
|
||||||
|
duration = options.Duration
|
||||||
|
}
|
||||||
|
if duration != 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx,
|
||||||
|
time.Duration(duration*float64(time.Second)))
|
||||||
|
} else {
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// get android system version
|
||||||
|
var sysVersion int
|
||||||
|
if systemVersion, err := ad.Device.SystemVersion(); err == nil {
|
||||||
|
if version, err := strconv.Atoi(systemVersion); err == nil {
|
||||||
|
sysVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sysVersion == 0 {
|
||||||
|
log.Warn().Err(err).Msg("get android system version failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var useAdbScreenRecord bool
|
||||||
|
audioOn := options.ScreenRecordWithAudio
|
||||||
|
if options.ScreenRecordWithScrcpy {
|
||||||
|
useAdbScreenRecord = false
|
||||||
|
} else if !audioOn {
|
||||||
|
log.Info().Bool("audioOn", audioOn).Msg("screen record with adb screenrecord by default")
|
||||||
|
useAdbScreenRecord = true
|
||||||
|
} else if sysVersion != 0 && sysVersion < 11 {
|
||||||
|
// scrcpy audio forwarding is supported for devices with Android 11 or higher
|
||||||
|
// https://github.com/Genymobile/scrcpy/blob/master/doc/audio.md
|
||||||
|
log.Warn().Bool("audioOn", audioOn).Int("version", sysVersion).
|
||||||
|
Msg("Audio disabled, it is only supported for Android >= 11, use adb screenrecord")
|
||||||
|
useAdbScreenRecord = true
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = file.Close()
|
if err == nil {
|
||||||
|
filePath, err = filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "get absolute path failed")
|
||||||
|
} else {
|
||||||
|
log.Info().Str("path", filePath).Msg("screen record success")
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// scrcpy -s 7d21bb91 --record=file.mp4 -N
|
if useAdbScreenRecord {
|
||||||
|
// screen record with adb screenrecord
|
||||||
|
// adb screenrecord duration is limited in range [1,180] seconds
|
||||||
|
res, err := ad.Device.ScreenRecord(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "screen record failed")
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filePath, res, 0o644); err != nil {
|
||||||
|
return "", errors.Wrap(err, "write screen record file failed")
|
||||||
|
}
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// screen record with scrcpy
|
||||||
|
log.Info().Float64("duration(s)", duration).Msg("screen record with scrcpy")
|
||||||
|
|
||||||
|
// start scrcpy
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"scrcpy",
|
"scrcpy",
|
||||||
"-s", ad.Device.Serial(),
|
"-s", ad.Device.Serial(),
|
||||||
fmt.Sprintf("--record=%s", fileName),
|
fmt.Sprintf("--record=%s", filePath),
|
||||||
"-N",
|
"--record-format=mp4",
|
||||||
|
"--max-fps=30",
|
||||||
|
"--no-playback", // Disable video and audio playback on the computer
|
||||||
)
|
)
|
||||||
cmd.Stdout = io.Discard
|
cmd.Stdout = io.Discard
|
||||||
cmd.Stderr = io.Discard
|
cmd.Stderr = io.Discard
|
||||||
// 启动命令
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Error().Err(err)
|
return "", errors.Wrap(err, "start screen record failed")
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
timer := time.After(duration)
|
|
||||||
|
|
||||||
done := make(chan error)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
// 等待 ffmpeg 命令执行完毕
|
|
||||||
done <- cmd.Wait()
|
done <- cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-timer:
|
case <-ctx.Done():
|
||||||
// 超时,停止 scrcpy 进程
|
// timeout or cancelled
|
||||||
|
log.Info().Msg("screen recording stopped")
|
||||||
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
|
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
|
||||||
log.Error().Err(err)
|
log.Error().Err(err).Msg("failed to stop scrcpy process")
|
||||||
|
_ = cmd.Process.Kill() // 强制结束进程
|
||||||
}
|
}
|
||||||
|
<-done // 等待进程完全退出
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
// ffmpeg 正常结束
|
log.Info().Msg("scrcpy exited")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err)
|
return "", errors.Wrap(err, "screen record with scrcpy failed")
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filepath.Abs(fileName)
|
|
||||||
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *ADBDriver) Setup() error {
|
func (ad *ADBDriver) Setup() error {
|
||||||
|
|||||||
+46
-3
@@ -3,6 +3,7 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -195,10 +196,52 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) {
|
|||||||
|
|
||||||
func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
||||||
driver := setupADBDriverExt(t)
|
driver := setupADBDriverExt(t)
|
||||||
path, err := driver.ScreenRecord(5 * time.Second)
|
|
||||||
|
// adb screenrecord --time-limit 5
|
||||||
|
path1, err := driver.ScreenRecord(
|
||||||
|
option.WithScreenRecordDuation(5))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(path1)
|
||||||
|
t.Log(path1)
|
||||||
|
|
||||||
|
// scrcpy with time limit
|
||||||
|
path2, err := driver.ScreenRecord(
|
||||||
|
option.WithScreenRecordDuation(5),
|
||||||
|
option.WithScreenRecordAudio(true),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(path2)
|
||||||
|
t.Log(path2)
|
||||||
|
|
||||||
|
// scrcpy with time limit
|
||||||
|
path3, err := driver.ScreenRecord(
|
||||||
|
option.WithScreenRecordDuation(5),
|
||||||
|
option.WithScreenRecordScrcpy(true),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(path3)
|
||||||
|
t.Log(path3)
|
||||||
|
|
||||||
|
// scrcpy with cancel signal
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
path4, err := driver.ScreenRecord(
|
||||||
|
option.WithContext(ctx),
|
||||||
|
option.WithScreenRecordScrcpy(true),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(path4)
|
||||||
|
t.Log(path4)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// record for 3 seconds
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err = <-done
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(path)
|
|
||||||
t.Log(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_ADB_Backspace(t *testing.T) {
|
func TestDriver_ADB_Backspace(t *testing.T) {
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ func (wd *BrowserDriver) GetSession() *DriverSession {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wd *BrowserDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
func (wd *BrowserDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
@@ -37,7 +36,7 @@ type IDriver interface {
|
|||||||
ForegroundInfo() (app types.AppInfo, err error)
|
ForegroundInfo() (app types.AppInfo, err error)
|
||||||
WindowSize() (types.Size, error)
|
WindowSize() (types.Size, error)
|
||||||
ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error)
|
ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error)
|
||||||
ScreenRecord(duration time.Duration) (videoPath string, err error)
|
ScreenRecord(opts ...option.ActionOption) (videoPath string, err error)
|
||||||
Source(srcOpt ...option.SourceOption) (string, error)
|
Source(srcOpt ...option.SourceOption) (string, error)
|
||||||
Orientation() (orientation types.Orientation, err error)
|
Orientation() (orientation types.Orientation, err error)
|
||||||
Rotation() (rotation types.Rotation, err error)
|
Rotation() (rotation types.Rotation, err error)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
|||||||
case ACTION_ScreenShot:
|
case ACTION_ScreenShot:
|
||||||
// take screenshot
|
// take screenshot
|
||||||
log.Info().Msg("take screenshot for current screen")
|
log.Info().Msg("take screenshot for current screen")
|
||||||
_, err := dExt.GetScreenResult(action.GetScreenOptions()...)
|
_, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
|
||||||
return err
|
return err
|
||||||
case ACTION_ClosePopups:
|
case ACTION_ClosePopups:
|
||||||
return dExt.ClosePopupsHandler()
|
return dExt.ClosePopupsHandler()
|
||||||
|
|||||||
@@ -364,11 +364,11 @@ func (s *StubIOSDriver) WindowSize() (types.Size, error) {
|
|||||||
return s.WDADriver.WindowSize()
|
return s.WDADriver.WindowSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StubIOSDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
func (s *StubIOSDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||||
if err := s.SetupWda(); err != nil {
|
if err := s.SetupWda(); err != nil {
|
||||||
return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||||||
}
|
}
|
||||||
return s.WDADriver.ScreenRecord(duration)
|
return s.WDADriver.ScreenRecord(opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StubIOSDriver) Orientation() (types.Orientation, error) {
|
func (s *StubIOSDriver) Orientation() (types.Orientation, error) {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func (hd *HDCDriver) StopCaptureLog() (result interface{}, err error) {
|
|||||||
return hd.points, nil
|
return hd.points, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hd *HDCDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
func (hd *HDCDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -875,10 +875,13 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
|
|||||||
return wd.Session.POST(data, "/gtf/automation/log")
|
return wd.Session.POST(data, "/gtf/automation/log")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wd *WDADriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
func (wd *WDADriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||||
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
||||||
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||||
|
|
||||||
|
options := option.NewActionOptions(opts...)
|
||||||
|
duration := time.Duration(options.Duration * float64(time.Second))
|
||||||
|
|
||||||
file, err := os.Create(fileName)
|
file, err := os.Create(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error creating file:", err)
|
fmt.Println("Error creating file:", err)
|
||||||
|
|||||||
+1
-1
@@ -302,7 +302,7 @@ func TestDriver_WDA_AccessibleSource(t *testing.T) {
|
|||||||
|
|
||||||
func TestDriver_WDA_ScreenRecord(t *testing.T) {
|
func TestDriver_WDA_ScreenRecord(t *testing.T) {
|
||||||
driver := setupWDADriverExt(t)
|
driver := setupWDADriverExt(t)
|
||||||
path, err := driver.ScreenRecord(5 * time.Second)
|
path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
t.Log(path)
|
t.Log(path)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -1,12 +1,14 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActionOptions struct {
|
type ActionOptions struct {
|
||||||
|
Context context.Context
|
||||||
// log
|
// log
|
||||||
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
|
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
|
||||||
|
|
||||||
@@ -33,6 +35,9 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.Context != nil {
|
||||||
|
options = append(options, WithContext(o.Context))
|
||||||
|
}
|
||||||
if o.Identifier != "" {
|
if o.Identifier != "" {
|
||||||
options = append(options, WithIdentifier(o.Identifier))
|
options = append(options, WithIdentifier(o.Identifier))
|
||||||
}
|
}
|
||||||
@@ -120,11 +125,10 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
options = append(options, o.GetScreenShotOptions()...)
|
||||||
}
|
options = append(options, o.GetScreenRecordOptions()...)
|
||||||
|
|
||||||
func (o *ActionOptions) GetScreenOptions() []ActionOption {
|
return options
|
||||||
return o.ScreenOptions.Options()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) {
|
func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) {
|
||||||
@@ -207,6 +211,12 @@ func NewActionOptions(opts ...ActionOption) *ActionOptions {
|
|||||||
|
|
||||||
type ActionOption func(o *ActionOptions)
|
type ActionOption func(o *ActionOptions)
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithCustomOption(key string, value interface{}) ActionOption {
|
func WithCustomOption(key string, value interface{}) ActionOption {
|
||||||
return func(o *ActionOptions) {
|
return func(o *ActionOptions) {
|
||||||
if o.Custom == nil {
|
if o.Custom == nil {
|
||||||
|
|||||||
+58
-2
@@ -1,9 +1,12 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/httprunner/httprunner/v5/uixt/types"
|
import (
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
)
|
||||||
|
|
||||||
type ScreenOptions struct {
|
type ScreenOptions struct {
|
||||||
ScreenShotOptions
|
ScreenShotOptions
|
||||||
|
ScreenRecordOptions
|
||||||
ScreenFilterOptions
|
ScreenFilterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ type ScreenShotOptions struct {
|
|||||||
ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"`
|
ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ScreenShotOptions) Options() []ActionOption {
|
func (o *ScreenShotOptions) GetScreenShotOptions() []ActionOption {
|
||||||
options := make([]ActionOption, 0)
|
options := make([]ActionOption, 0)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
return options
|
return options
|
||||||
@@ -125,6 +128,59 @@ func WithScreenShotFileName(fileName string) ActionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScreenRecordOptions struct {
|
||||||
|
ScreenRecordDuration float64 `json:"screenrecord_duration,omitempty" yaml:"screenrecord_duration,omitempty"`
|
||||||
|
ScreenRecordWithAudio bool `json:"screenrecord_with_audio,omitempty" yaml:"screenrecord_with_audio,omitempty"`
|
||||||
|
ScreenRecordWithScrcpy bool `json:"screenrecord_with_scrcpy,omitempty" yaml:"screenrecord_with_scrcpy,omitempty"`
|
||||||
|
ScreenRecordPath string `json:"screenrecord_path,omitempty" yaml:"screenrecord_path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ScreenRecordOptions) GetScreenRecordOptions() []ActionOption {
|
||||||
|
options := make([]ActionOption, 0)
|
||||||
|
if o == nil {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// screen record options
|
||||||
|
if o.ScreenRecordDuration > 0 {
|
||||||
|
options = append(options, WithDuration(o.ScreenRecordDuration))
|
||||||
|
}
|
||||||
|
if o.ScreenRecordWithAudio {
|
||||||
|
options = append(options, WithScreenRecordAudio(true))
|
||||||
|
}
|
||||||
|
if o.ScreenRecordWithScrcpy {
|
||||||
|
options = append(options, WithScreenRecordScrcpy(true))
|
||||||
|
}
|
||||||
|
if o.ScreenRecordPath != "" {
|
||||||
|
options = append(options, WithScreenRecordPath(o.ScreenRecordPath))
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScreenRecordDuation(duration float64) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenRecordDuration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScreenRecordAudio(audioOn bool) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenRecordWithAudio = audioOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScreenRecordScrcpy(scrcpyOn bool) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenRecordWithScrcpy = scrcpyOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScreenRecordPath(path string) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenRecordPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner
|
// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner
|
||||||
// [x1, y1, x2, y2] in percentage of the screen
|
// [x1, y1, x2, y2] in percentage of the screen
|
||||||
type Scope []float64
|
type Scope []float64
|
||||||
|
|||||||
Reference in New Issue
Block a user