refactor ios: replace gidevice with go-ios

This commit is contained in:
lilong.129
2024-11-23 14:12:07 +08:00
parent 8aac2181be
commit ef37d88e0b
118 changed files with 2202 additions and 11764 deletions

View File

@@ -15,24 +15,25 @@ import (
type ActionMethod string
const (
ACTION_LOG ActionMethod = "log"
ACTION_AppInstall ActionMethod = "install"
ACTION_AppUninstall ActionMethod = "uninstall"
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_StartCamera ActionMethod = "camera_start" // alias for app_launch camera
ACTION_StopCamera ActionMethod = "camera_stop" // alias for app_terminate camera
ACTION_SetClipboard ActionMethod = "set_clipboard"
ACTION_GetClipboard ActionMethod = "get_clipboard"
ACTION_SetIme ActionMethod = "set_ime"
ACTION_GetSource ActionMethod = "get_source"
ACTION_LOG ActionMethod = "log"
ACTION_AppInstall ActionMethod = "install"
ACTION_AppUninstall ActionMethod = "uninstall"
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_StartCamera ActionMethod = "camera_start" // alias for app_launch camera
ACTION_StopCamera ActionMethod = "camera_stop" // alias for app_terminate camera
ACTION_SetClipboard ActionMethod = "set_clipboard"
ACTION_GetClipboard ActionMethod = "get_clipboard"
ACTION_SetIme ActionMethod = "set_ime"
ACTION_GetSource ActionMethod = "get_source"
ACTION_GetForegroundApp ActionMethod = "get_foreground_app"
// UI handling
ACTION_Home ActionMethod = "home"
@@ -46,6 +47,7 @@ const (
ACTION_Swipe ActionMethod = "swipe"
ACTION_Input ActionMethod = "input"
ACTION_Back ActionMethod = "back"
ACTION_KeyCode ActionMethod = "keycode"
// custom actions
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
@@ -109,7 +111,8 @@ type ActionOptions struct {
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times
IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found
Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty"` // interval between retries in seconds
PressDuration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration of ios swipe action
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration of ios swipe action
PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty"` // used to set duration of ios swipe action
Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of android swipe action
Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action
@@ -159,6 +162,9 @@ func (o *ActionOptions) Options() []ActionOption {
if o.Interval != 0 {
options = append(options, WithInterval(o.Interval))
}
if o.Duration != 0 {
options = append(options, WithDuration(o.Duration))
}
if o.PressDuration != 0 {
options = append(options, WithPressDuration(o.PressDuration))
}
@@ -309,8 +315,8 @@ func (o *ActionOptions) updateData(data map[string]interface{}) {
data["steps"] = 12 // default steps
}
if o.PressDuration > 0 {
data["duration"] = o.PressDuration
if o.Duration > 0 {
data["duration"] = o.Duration
}
if _, ok := data["duration"]; !ok {
data["duration"] = 0 // default duration
@@ -380,9 +386,15 @@ func WithInterval(sec float64) ActionOption {
}
}
func WithPressDuration(duration float64) ActionOption {
func WithDuration(duration float64) ActionOption {
return func(o *ActionOptions) {
o.PressDuration = duration
o.Duration = duration
}
}
func WithPressDuration(pressDuration float64) ActionOption {
return func(o *ActionOptions) {
o.PressDuration = pressDuration
}
}

View File

@@ -6,12 +6,15 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/httprunner/funplugin/myexec"
@@ -245,6 +248,34 @@ func (ad *adbDriver) Unlock() (err error) {
return ad.PressKeyCodes(KCMenu, KMEmpty)
}
func (ad *adbDriver) Backspace(count int, options ...ActionOption) (err error) {
if count == 0 {
return nil
}
if count == 1 {
return ad.PressKeyCode(67)
}
keyArray := make([]KeyCode, count)
for i := range keyArray {
keyArray[i] = KeyCode(67)
}
return ad.combinationKey(keyArray)
}
func (ad *adbDriver) combinationKey(keyCodes []KeyCode) (err error) {
if len(keyCodes) == 1 {
return ad.PressKeyCode(keyCodes[0])
}
strKeyCodes := make([]string, len(keyCodes))
for i, keycode := range keyCodes {
strKeyCodes[i] = fmt.Sprintf("%d", keycode)
}
_, err = ad.adbClient.RunShellCommand(
"input", append([]string{"keycombination"}, strKeyCodes...)...)
return
}
func (ad *adbDriver) PressKeyCode(keyCode KeyCode) (err error) {
return ad.PressKeyCodes(keyCode, KMEmpty)
}
@@ -309,15 +340,11 @@ func (ad *adbDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
return nil
}
func (ad *adbDriver) DoubleTap(x, y int, options ...ActionOption) error {
return ad.DoubleTapFloat(float64(x), float64(y), options...)
}
func (ad *adbDriver) DoubleTapFloat(x, y float64, options ...ActionOption) (err error) {
func (ad *adbDriver) DoubleTap(x, y float64, options ...ActionOption) error {
// adb shell input tap x y
xStr := fmt.Sprintf("%.1f", x)
yStr := fmt.Sprintf("%.1f", y)
_, err = ad.adbClient.RunShellCommand(
_, err := ad.adbClient.RunShellCommand(
"input", "tap", xStr, yStr)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("tap <%s, %s> failed", xStr, yStr))
@@ -331,22 +358,64 @@ func (ad *adbDriver) DoubleTapFloat(x, y float64, options ...ActionOption) (err
return nil
}
func (ad *adbDriver) TouchAndHold(x, y int, second ...float64) (err error) {
return ad.TouchAndHoldFloat(float64(x), float64(y), second...)
func (ad *adbDriver) TouchAndHold(x, y float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
if len(actionOptions.Offset) == 2 {
x += float64(actionOptions.Offset[0])
y += float64(actionOptions.Offset[1])
}
x += actionOptions.getRandomOffset()
y += actionOptions.getRandomOffset()
duration := 1000.0
if actionOptions.Duration > 0 {
duration = actionOptions.Duration * 1000
}
// adb shell input swipe fromX fromY toX toY
_, err = ad.adbClient.RunShellCommand(
"input", "swipe",
fmt.Sprintf("%.1f", x), fmt.Sprintf("%.1f", y),
fmt.Sprintf("%.1f", x), fmt.Sprintf("%.1f", y),
fmt.Sprintf("%d", int(duration)),
)
if err != nil {
return errors.Wrap(err, "long press failed")
}
return nil
}
func (ad *adbDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
err = errDriverNotImplemented
return
}
func (ad *adbDriver) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
func (ad *adbDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ad.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ad *adbDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
err = errDriverNotImplemented
return
if len(actionOptions.Offset) == 4 {
fromX += float64(actionOptions.Offset[0])
fromY += float64(actionOptions.Offset[1])
toX += float64(actionOptions.Offset[2])
toY += float64(actionOptions.Offset[3])
}
fromX += actionOptions.getRandomOffset()
fromY += actionOptions.getRandomOffset()
toX += actionOptions.getRandomOffset()
toY += actionOptions.getRandomOffset()
duration := 200.0
if actionOptions.Duration > 0 {
duration = actionOptions.Duration * 1000
}
command := "swipe"
if actionOptions.PressDuration > 0 {
command = "draganddrop"
}
// adb shell input swipe fromX fromY toX toY
_, err = ad.adbClient.RunShellCommand(
"input", command,
fmt.Sprintf("%.1f", fromX), fmt.Sprintf("%.1f", fromY),
fmt.Sprintf("%.1f", toX), fmt.Sprintf("%.1f", toY),
fmt.Sprintf("%d", int(duration)),
)
if err != nil {
return errors.Wrap(err, "drag failed")
}
return nil
}
func (ad *adbDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
@@ -557,8 +626,8 @@ func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
return
}
func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
return errDriverNotImplemented
func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
return info, errDriverNotImplemented
}
func (ad *adbDriver) LogoutNoneUI(packageName string) error {
@@ -750,6 +819,10 @@ func (ad *adbDriver) GetSession() *DriverSession {
return &ad.Driver.session
}
func (ad *adbDriver) GetDriverResults() []*DriverResult {
return nil
}
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
packageInfo, err := ad.adbClient.RunShellCommand(
"CLASSPATH=/data/local/tmp/evalite", "app_process", "/",
@@ -916,3 +989,63 @@ var androidActivities = map[string]map[string][]string{
},
// TODO: SPH, XHS
}
func (ad *adbDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) {
// 获取当前时间戳
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
// 创建文件名
fileName := fmt.Sprintf("%s/%s.mp4", folderPath, timestamp)
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
log.Error().Err(err).Msg("Error creating directory")
}
// 创建一个文件
file, err := os.Create(fileName)
if err != nil {
log.Error().Err(err)
return "", err
}
defer func() {
_ = file.Close()
}()
// scrcpy -s 7d21bb91 --record=file.mp4 -N
cmd := exec.Command(
"scrcpy",
"-s", ad.adbClient.Serial(),
fmt.Sprintf("--record=%s", fileName),
"-N",
)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
// 启动命令
if err := cmd.Start(); err != nil {
log.Error().Err(err)
return "", err
}
timer := time.After(duration)
done := make(chan error)
go func() {
// 等待 ffmpeg 命令执行完毕
done <- cmd.Wait()
}()
select {
case <-timer:
// 超时,停止 scrcpy 进程
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
log.Error().Err(err)
}
case err := <-done:
// ffmpeg 正常结束
if err != nil {
log.Error().Err(err)
return "", err
}
}
return filepath.Abs(fileName)
}
func (ad *adbDriver) TearDown() {
}

View File

@@ -24,6 +24,12 @@ type stubAndroidDriver struct {
const StubSocketName = "com.bytest.device"
type AppLoginInfo struct {
Did string `json:"did,omitempty" yaml:"did,omitempty"`
Uid string `json:"uid,omitempty" yaml:"uid,omitempty"`
IsLogin bool `json:"is_login,omitempty" yaml:"is_login,omitempty"`
}
// newStubAndroidDriver
// 创建stub Driver address为forward后的端口格式127.0.0.1:${port}
func newStubAndroidDriver(address string, urlPrefix string, readTimeout ...time.Duration) (*stubAndroidDriver, error) {
@@ -174,52 +180,37 @@ func (sad *stubAndroidDriver) Source(srcOpt ...SourceOption) (source string, err
return res.(string), nil
}
func (sad *stubAndroidDriver) LoginNoneUIBak(packageName, phoneNumber, captcha string) error {
_, err := sad.adbClient.RunShellCommand(
"am", "broadcast",
"-a", fmt.Sprintf("%s.util.crony.action_login", packageName),
"-e", "phone", phoneNumber,
"-e", "code", captcha)
if err != nil {
return err
}
time.Sleep(10 * time.Second)
login, err := sad.isLogin(packageName)
if err != nil || !login {
log.Err(err).Msg("failed to login")
return fmt.Errorf("failed to login")
}
return err
}
func (sad *stubAndroidDriver) LoginNoneUI(packageName, phoneNumber, captcha string) error {
func (sad *stubAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
params := map[string]interface{}{
"phone": phoneNumber,
"code": captcha,
}
if captcha != "" {
params["captcha"] = captcha
} else if password != "" {
params["password"] = password
} else {
return info, fmt.Errorf("password and capcha is empty")
}
resp, err := sad.httpPOST(params, "/host", "/login", "account")
if err != nil {
return err
return info, err
}
res, err := resp.valueConvertToJsonObject()
if err != nil {
return err
return info, err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("failed to login %s", res["data"])
err = fmt.Errorf("falied to login %s", res["data"])
log.Err(err).Msgf("%v", res)
return err
return info, err
}
time.Sleep(10 * time.Second)
login, err := sad.isLogin(packageName)
if err != nil {
return err
time.Sleep(20 * time.Second)
info, err = sad.getLoginAppInfo(packageName)
if err != nil || !info.IsLogin {
return info, fmt.Errorf("falied to login %v", info)
}
if !login {
return fmt.Errorf("failed to login")
}
return nil
return info, nil
}
func (sad *stubAndroidDriver) LogoutNoneUI(packageName string) error {
@@ -233,11 +224,15 @@ func (sad *stubAndroidDriver) LogoutNoneUI(packageName string) error {
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("failed to logout %s", res["data"])
err = fmt.Errorf("falied to logout %s", res["data"])
log.Err(err).Msgf("%v", res)
return err
}
log.Info().Interface("resp", resp).Msg("logout success")
fmt.Printf("%v", resp)
if err != nil {
return err
}
time.Sleep(3 * time.Second)
return nil
}
@@ -256,21 +251,44 @@ func (sad *stubAndroidDriver) LoginNoneUIDynamic(packageName, phoneNumber string
return nil
}
func (sad *stubAndroidDriver) isLogin(packageName string) (login bool, err error) {
resp, err := sad.httpGET("/host", "/login", "/check")
func (sad *stubAndroidDriver) SetHDTStatus(status bool) error {
_, err := sad.adbClient.RunShellCommand("settings", "put", "global", "feedbacker_sso_bypass_token", "default_sso_bypass_token")
if err != nil {
return false, err
log.Warn().Msg(fmt.Sprintf("failed to disable sso, error: %v", err))
}
params := map[string]interface{}{
"ClassName": "com.bytedance.ies.stark.framework.HybridDevTool",
"Method": "setEnabled",
"RetType": "",
"Args": []bool{status},
}
res, err := sad.sendCommand("com.ss.android.ugc.aweme", "CallStaticMethod", params)
if err != nil {
return fmt.Errorf("failed to set hds status %v, error: %v", status, err)
}
log.Info().Msg(fmt.Sprintf("set hdt status result: %s", res))
return nil
}
func (sad *stubAndroidDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) {
resp, err := sad.httpGET("/host", "/app", "/info")
if err != nil {
return info, err
}
res, err := resp.valueConvertToJsonObject()
if err != nil {
return false, err
return info, err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("failed to check login %s", res["data"])
err = fmt.Errorf("falied to get app info %s", res["data"])
log.Err(err).Msgf("%v", res)
return false, err
return info, err
}
log.Info().Interface("resp", resp).Msg("check login success")
return true, nil
err = json.Unmarshal([]byte(res["data"].(string)), &info)
if err != nil {
err = fmt.Errorf("falied to parse app info %s", res["data"])
return
}
return info, nil
}

View File

@@ -1,6 +1,10 @@
package uixt
import "testing"
import (
"fmt"
"os"
"testing"
)
var androidStubDriver *stubAndroidDriver
@@ -30,21 +34,13 @@ func TestSource(t *testing.T) {
t.Log(source)
}
func TestIsLogin(t *testing.T) {
setupStubDriver(t)
res, err := androidStubDriver.isLogin("com.ss.android.ugc.aweme")
if err != nil {
t.Fatal(err)
}
t.Log(res)
}
func TestLogin(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.LoginNoneUI("com.ss.android.ugc.aweme", "12342316231", "8517")
info, err := androidStubDriver.LoginNoneUI("com.ss.android.ugc.aweme", "12342316231", "8517", "")
if err != nil {
t.Fatal(err)
}
t.Log(info)
}
func TestLogout(t *testing.T) {
@@ -54,3 +50,92 @@ func TestLogout(t *testing.T) {
t.Fatal(err)
}
}
func TestSwipe(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.Swipe(878, 2375, 672, 2375)
if err != nil {
t.Fatal(err)
}
}
func TestTap(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.Tap(900, 400)
if err != nil {
t.Fatal(err)
}
}
func TestDoubleTap(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.DoubleTap(500, 500)
if err != nil {
t.Fatal(err)
}
}
func TestLongPress(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.Swipe(1036, 1076, 1036, 1076, WithDuration(3))
if err != nil {
t.Fatal(err)
}
}
func TestInput(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.Input("\"哈哈\"")
if err != nil {
t.Fatal(err)
}
}
func TestSave(t *testing.T) {
setupStubDriver(t)
raw, err := androidStubDriver.Screenshot()
if err != nil {
t.Fatal(err)
}
source, err := androidStubDriver.Source()
if err != nil {
t.Fatal(err)
}
step := 14
file, err := os.Create(fmt.Sprintf("/Users/bytedance/workcode/wings_algorithm/testcases/data/cases/0/%d.jpg", step))
if err != nil {
t.Fatal(err)
}
file.Write(raw.Bytes())
file, err = os.Create(fmt.Sprintf("/Users/bytedance/workcode/wings_algorithm/testcases/data/cases/0/%d.json", step))
if err != nil {
t.Fatal(err)
}
file.Write([]byte(source))
}
func TestAppLaunch(t *testing.T) {
setupStubDriver(t)
err := androidStubDriver.AppLaunch("com.ss.android.ugc.aweme")
if err != nil {
t.Fatal(err)
}
}
func TestAppTerminal(t *testing.T) {
setupStubDriver(t)
_, err := androidStubDriver.AppTerminate("com.ss.android.ugc.aweme")
if err != nil {
t.Fatal(err)
}
}
func TestAppInfo(t *testing.T) {
setupStubDriver(t)
info, err := androidStubDriver.getLoginAppInfo("com.ss.android.ugc.aweme")
if err != nil {
t.Fatal(err)
}
t.Log(info)
}

View File

@@ -262,7 +262,7 @@ func TestDriver_Drag(t *testing.T) {
}
time.Sleep(time.Millisecond * 200)
err = driver.DragFloat(400, 501.5, 400, 261.5)
err = driver.Drag(400, 501.5, 400, 261.5)
if err != nil {
t.Fatal(err)
}
@@ -502,3 +502,21 @@ func TestTapTexts(t *testing.T) {
t.Fatal(err)
}
}
func TestRecordVideo(t *testing.T) {
setupAndroidAdbDriver(t)
path, err := driverExt.Driver.(*adbDriver).RecordScreen("", 5*time.Second)
if err != nil {
t.Fatal(err)
}
println(path)
}
func Test_Android_Backspace(t *testing.T) {
setupAndroidAdbDriver(t)
err := driverExt.Driver.Backspace(1)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -294,8 +294,8 @@ func (ud *uiaDriver) Orientation() (orientation Orientation, err error) {
return
}
func (ud *uiaDriver) DoubleTap(x, y int, options ...ActionOption) error {
return ud.DoubleFloatTap(float64(x), float64(y))
func (ud *uiaDriver) DoubleTap(x, y float64, options ...ActionOption) error {
return ud.DoubleFloatTap(x, y)
}
func (ud *uiaDriver) DoubleFloatTap(x, y float64) error {
@@ -362,20 +362,18 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
return err
}
func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
return ud.TouchAndHoldFloat(float64(x), float64(y), second...)
}
func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
if len(second) == 0 {
second = []float64{1.0}
func (ud *uiaDriver) TouchAndHold(x, y float64, options ...ActionOption) (err error) {
opts := NewActionOptions(options...)
duration := opts.Duration
if duration == 0 {
duration = 1.0
}
// register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick"))
data := map[string]interface{}{
"params": map[string]interface{}{
"x": x,
"y": y,
"duration": int(second[0] * 1000),
"duration": int(duration * 1000),
},
}
_, err = ud.httpPOST(data, "/session", ud.session.ID, "touch/longclick")
@@ -386,11 +384,7 @@ func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err err
// the smoothness and speed of the swipe by specifying the number of steps.
// Each step execution is throttled to 5 milliseconds per step, so for a 100
// steps, the swipe will take around 0.5 seconds to complete.
func (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return ud.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
func (ud *uiaDriver) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
if len(actionOptions.Offset) == 4 {
fromX += float64(actionOptions.Offset[0])
@@ -662,3 +656,10 @@ func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
}
return nil
}
func (ud *uiaDriver) GetDriverResults() []*DriverResult {
defer func() {
ud.Driver.driverResults = nil
}()
return ud.Driver.driverResults
}

View File

@@ -62,8 +62,9 @@ type Driver struct {
client *http.Client
// cache to avoid repeated query
scale float64
windowSize *Size
scale float64
windowSize *Size
driverResults []*DriverResult
// cache session data
session DriverSession

View File

@@ -1,27 +1,42 @@
package uixt
func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) {
return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...)
import (
"fmt"
"github.com/rs/zerolog/log"
)
func (dExt *DriverExt) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
return dExt.Driver.Drag(fromX, fromY, toX, toY, options...)
}
func (dExt *DriverExt) DragFloat(pathname string, toX, toY float64, pressForDuration ...float64) (err error) {
return dExt.DragOffsetFloat(pathname, toX, toY, 0, 0, pressForDuration...)
}
func (dExt *DriverExt) DragOffset(pathname string, toX, toY int, xOffset, yOffset float64, pressForDuration ...float64) (err error) {
return dExt.DragOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset, pressForDuration...)
}
func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64, pressForDuration ...float64) (err error) {
if len(pressForDuration) == 0 {
pressForDuration = []float64{1.0}
}
point, err := dExt.FindUIRectInUIKit(pathname)
func (dExt *DriverExt) DragRelative(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
width := dExt.windowSize.Width
height := dExt.windowSize.Height
orientation, err := dExt.Driver.Orientation()
if err != nil {
return err
log.Warn().Err(err).Msgf("drag from (%v, %v) to (%v, %v) get orientation failed, use default orientation",
fromX, fromY, toX, toY)
orientation = OrientationPortrait
}
return dExt.Driver.DragFloat(point.X+xOffset, point.Y+yOffset, toX, toY,
WithPressDuration(pressForDuration[0]))
if !assertRelative(fromX) || !assertRelative(fromY) ||
!assertRelative(toX) || !assertRelative(toY) {
return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1",
fromX, fromY, toX, toY)
}
// 左转和右转都是"LANDSCAPE"
if orientation == OrientationPortrait {
fromX = float64(width) * fromX
fromY = float64(height) * fromY
toX = float64(width) * toX
toY = float64(height) * toY
} else {
fromX = float64(height) * fromX
fromY = float64(width) * fromY
toX = float64(height) * toX
toY = float64(width) * toY
}
return dExt.Driver.Drag(fromX, fromY, toX, toY, options...)
}

View File

@@ -20,6 +20,7 @@ type DriverExt struct {
Driver IWebDriver
ImageService IImageService // used to extract image data
windowSize Size
// funplugin
plugin funplugin.IPlugin
}

View File

@@ -145,22 +145,6 @@ func (dev *HarmonyDevice) NewUSBDriver(options ...DriverOption) (driver IWebDriv
return harmonyDriver, nil
}
func (dev *HarmonyDevice) StartPerf() error {
return nil
}
func (dev *HarmonyDevice) StopPerf() string {
return ""
}
func (dev *HarmonyDevice) StartPcap() error {
return nil
}
func (dev *HarmonyDevice) StopPcap() string {
return ""
}
func (dev *HarmonyDevice) Install(appPath string, options ...InstallOption) error {
return nil
}

View File

@@ -175,27 +175,15 @@ func (hd *hdcDriver) TapFloat(x, y float64, options ...ActionOption) error {
return hd.uiDriver.InjectGesture(ghdc.NewGesture().Start(ghdc.Point{X: int(x), Y: int(y)}).Pause(100))
}
func (hd *hdcDriver) DoubleTap(x, y int, options ...ActionOption) error {
func (hd *hdcDriver) DoubleTap(x, y float64, options ...ActionOption) error {
return errDriverNotImplemented
}
func (hd *hdcDriver) DoubleTapFloat(x, y float64, options ...ActionOption) error {
func (hd *hdcDriver) TouchAndHold(x, y float64, options ...ActionOption) (err error) {
return errDriverNotImplemented
}
func (hd *hdcDriver) TouchAndHold(x, y int, second ...float64) error {
return errDriverNotImplemented
}
func (hd *hdcDriver) TouchAndHoldFloat(x, y float64, second ...float64) error {
return errDriverNotImplemented
}
func (hd *hdcDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return errDriverNotImplemented
}
func (hd *hdcDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
func (hd *hdcDriver) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) error {
return errDriverNotImplemented
}
@@ -260,6 +248,10 @@ func (hd *hdcDriver) PressBack(options ...ActionOption) error {
return hd.uiDriver.PressBack()
}
func (hd *hdcDriver) Backspace(count int, options ...ActionOption) (err error) {
return nil
}
func (hd *hdcDriver) PressKeyCode(keyCode KeyCode) (err error) {
return errDriverNotImplemented
}
@@ -292,8 +284,9 @@ func (hd *hdcDriver) Source(srcOpt ...SourceOption) (string, error) {
return "", nil
}
func (hd *hdcDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
return errDriverNotImplemented
func (hd *hdcDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
err = errDriverNotImplemented
return
}
func (hd *hdcDriver) LogoutNoneUI(packageName string) error {
@@ -336,3 +329,14 @@ func (hd *hdcDriver) StopCaptureLog() (result interface{}, err error) {
// defer clear(hd.points)
return hd.points, nil
}
func (hd *hdcDriver) GetDriverResults() []*DriverResult {
return nil
}
func (hd *hdcDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) {
return "", nil
}
func (hd *hdcDriver) TearDown() {
}

View File

@@ -41,7 +41,7 @@ func TestHarmonyTap(t *testing.T) {
}
}
func TestSwipe(t *testing.T) {
func TestHarmonySwipe(t *testing.T) {
setupHarmonyDevice(t)
err := harmonyDriverExt.SwipeLeft()
if err != nil {
@@ -49,7 +49,7 @@ func TestSwipe(t *testing.T) {
}
}
func TestInput(t *testing.T) {
func TestHarmonyInput(t *testing.T) {
setupHarmonyDevice(t)
err := harmonyDriverExt.Input("test")
if err != nil {

View File

@@ -446,12 +446,14 @@ type DriverOptions struct {
plugin funplugin.IPlugin
withImageService bool
withResultFolder bool
withUIAction bool
}
func NewDriverOptions() *DriverOptions {
return &DriverOptions{
withImageService: true,
withResultFolder: true,
withUIAction: true,
}
}
@@ -475,6 +477,12 @@ func WithDriverResultFolder(withResultFolder bool) DriverOption {
}
}
func WithUIAction(withUIAction bool) DriverOption {
return func(options *DriverOptions) {
options.withUIAction = withUIAction
}
}
func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
return func(options *DriverOptions) {
options.plugin = plugin
@@ -490,17 +498,13 @@ type IDevice interface {
// TODO: add ctx to NewDriver
NewDriver(...DriverOption) (driverExt *DriverExt, err error)
StartPerf() error
StopPerf() string
StartPcap() error
StopPcap() string
Install(appPath string, options ...InstallOption) error
Uninstall(packageName string) error
GetPackageInfo(packageName string) (AppInfo, error)
GetCurrentWindow() (windowInfo WindowInfo, err error)
// Teardown() error
}
type ForegroundApp struct {
@@ -577,18 +581,15 @@ type IWebDriver interface {
TapFloat(x, y float64, options ...ActionOption) error
// DoubleTap Sends a double tap event at the coordinate.
DoubleTap(x, y int, options ...ActionOption) error
DoubleTapFloat(x, y float64, options ...ActionOption) error
DoubleTap(x, y float64, options ...ActionOption) error
// TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration.
// second: The default value is 1
TouchAndHold(x, y int, second ...float64) error
TouchAndHoldFloat(x, y float64, second ...float64) error
TouchAndHold(x, y float64, options ...ActionOption) error
// Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate.
// WithPressDurationOption option can be used to set pressForDuration (default to 1 second).
Drag(fromX, fromY, toX, toY int, options ...ActionOption) error
DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error
Drag(fromX, fromY, toX, toY float64, options ...ActionOption) error
// Swipe works like Drag, but `pressForDuration` value is 0
Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error
@@ -620,12 +621,14 @@ type IWebDriver interface {
PressKeyCode(keyCode KeyCode) (err error)
Backspace(count int, options ...ActionOption) (err error)
Screenshot() (*bytes.Buffer, error)
// Source Return application elements tree
Source(srcOpt ...SourceOption) (string, error)
LoginNoneUI(packageName, phoneNumber string, captcha string) error
LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error)
LogoutNoneUI(packageName string) error
TapByText(text string, options ...ActionOption) error
@@ -647,4 +650,9 @@ type IWebDriver interface {
// triggers the log capture and returns the log entries
StartCaptureLog(identifier ...string) (err error)
StopCaptureLog() (result interface{}, err error)
GetDriverResults() []*DriverResult
RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error)
TearDown()
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,17 @@ import (
"encoding/base64"
builtinJSON "encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/pkg/errors"
@@ -18,35 +24,32 @@ import (
"github.com/httprunner/httprunner/v4/hrp/code"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
)
type wdaDriver struct {
Driver
// default port
defaultConn gidevice.InnerConn
// mjpeg port
mjpegUSBConn gidevice.InnerConn // via USB
mjpegHTTPConn net.Conn // via HTTP
udid string
device *IOSDevice
mjpegHTTPConn net.Conn // via HTTP
mjpegClient *http.Client
mjpegUrl string
}
func (wd *wdaDriver) resetSession() error {
capabilities := NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
_, err := wd.NewSession(capabilities)
sessionInfo, err := wd.NewSession(capabilities)
if err != nil {
return err
}
wd.session.ID = sessionInfo.SessionId
return nil
}
func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte, disableRetry ...bool) (rawResp rawResponse, err error) {
disableRetryBool := len(disableRetry) > 0 && disableRetry[0]
for retryCount := 1; retryCount <= 5; retryCount++ {
for retryCount := 1; retryCount <= 2; retryCount++ {
rawResp, err = wd.Driver.httpRequest(method, rawURL, rawBody)
if err == nil || disableRetryBool {
return
@@ -108,19 +111,10 @@ func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionI
if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil {
return SessionInfo{}, err
}
wd.Driver.session.Reset()
wd.Driver.session.ID = sessionInfo.SessionId
return
}
func (wd *wdaDriver) DeleteSession() (err error) {
if wd.defaultConn != nil {
wd.defaultConn.Close()
}
if wd.mjpegUSBConn != nil {
wd.mjpegUSBConn.Close()
}
if wd.mjpegClient != nil {
wd.mjpegClient.CloseIdleConnections()
}
@@ -399,6 +393,9 @@ func (wd *wdaDriver) AppLaunch(bundleId string) (err error) {
// [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)]
data := make(map[string]interface{})
data["bundleId"] = bundleId
data["environment"] = map[string]interface{}{
"SHOW_EXPLORER": "NO",
}
_, err = wd.httpPOST(data, "/session", wd.session.ID, "/wda/apps/launch")
if err != nil {
return errors.Wrap(code.MobileUILaunchAppError,
@@ -448,20 +445,30 @@ func (wd *wdaDriver) AppDeactivate(second float64) (err error) {
return
}
func (wd *wdaDriver) GetForegroundApp() (app AppInfo, err error) {
// appInfo, err := wd.ActiveAppInfo()
// if err != nil {
// return AppInfo{}, err
// }
// app = AppInfo{
// AppBaseInfo: AppBaseInfo{
// PackageName: appInfo.BundleId,
// Activity: "",
// },
// }
return AppInfo{}, errors.Wrap(errDriverNotImplemented,
"GetForegroundApp not implemented for ios")
func (wd *wdaDriver) GetForegroundApp() (appInfo AppInfo, err error) {
activeAppInfo, err := wd.ActiveAppInfo()
appInfo.BundleId = activeAppInfo.BundleId
if err != nil {
return appInfo, err
}
apps, err := wd.device.ListApps(ApplicationTypeAny)
if err != nil {
return appInfo, err
}
for _, app := range apps {
if app.CFBundleIdentifier == activeAppInfo.BundleId {
appInfo.BundleId = app.CFBundleIdentifier
appInfo.AppName = app.CFBundleName
appInfo.VersionName = app.CFBundleShortVersionString
appInfo.PackageName = app.CFBundleIdentifier
versionCode, err := strconv.Atoi(app.CFBundleVersion)
if err == nil {
appInfo.VersionCode = versionCode
}
return appInfo, err
}
}
return appInfo, err
}
func (wd *wdaDriver) AssertForegroundApp(bundleId string, viewControllerType ...string) error {
@@ -499,43 +506,35 @@ func (wd *wdaDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
return
}
func (wd *wdaDriver) DoubleTap(x, y int, options ...ActionOption) error {
return wd.DoubleTapFloat(float64(x), float64(y), options...)
}
func (wd *wdaDriver) DoubleTapFloat(x, y float64, options ...ActionOption) (err error) {
func (wd *wdaDriver) DoubleTap(x, y float64, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)]
actionOptions := NewActionOptions(options...)
x = wd.toScale(x)
y = wd.toScale(y)
if len(actionOptions.Offset) == 2 {
x += float64(actionOptions.Offset[0])
y += float64(actionOptions.Offset[1])
}
x += actionOptions.getRandomOffset()
y += actionOptions.getRandomOffset()
data := map[string]interface{}{
"x": wd.toScale(x),
"y": wd.toScale(y),
"x": x,
"y": y,
}
_, err = wd.httpPOST(data, "/session", wd.session.ID, "/wda/doubleTap")
return
}
func (wd *wdaDriver) TouchAndHold(x, y int, second ...float64) error {
return wd.TouchAndHoldFloat(float64(x), float64(y), second...)
}
func (wd *wdaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
// [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)]
data := map[string]interface{}{
"x": wd.toScale(x),
"y": wd.toScale(y),
func (wd *wdaDriver) TouchAndHold(x, y float64, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
if actionOptions.Duration == 0 {
options = append(options, WithDuration(1))
}
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
}
data["duration"] = second[0]
_, err = wd.httpPOST(data, "/session", wd.session.ID, "/wda/touchAndHold")
return
return wd.TapFloat(x, y, options...)
}
func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error {
return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
func (wd *wdaDriver) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)]
actionOptions := NewActionOptions(options...)
@@ -560,11 +559,15 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...Action
"toX": toX,
"toY": toY,
}
if actionOptions.PressDuration > 0 {
data["pressDuration"] = actionOptions.PressDuration
}
// update data options in post data for extra WDA configurations
actionOptions.updateData(data)
// wda 43 version
_, err = wd.httpPOST(data, "/session", wd.session.ID, "/wda/dragfromtoforduration")
// _, err = wd.httpPOST(data, "/session", wd.session.ID, "/wda/drag")
return
}
@@ -573,7 +576,7 @@ func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption)
}
func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
return wd.DragFloat(fromX, fromY, toX, toY, options...)
return wd.Drag(fromX, fromY, toX, toY, options...)
}
func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) {
@@ -619,6 +622,20 @@ func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error)
return
}
func (wd *wdaDriver) Backspace(count int, options ...ActionOption) (err error) {
if count == 0 {
return nil
}
actionOptions := NewActionOptions(options...)
data := map[string]interface{}{"count": count}
// new data options in post data for extra WDA configurations
actionOptions.updateData(data)
_, err = wd.httpPOST(data, "/gtf/interaction/input/backspace")
return
}
func (wd *wdaDriver) Input(text string, options ...ActionOption) (err error) {
return wd.SendKeys(text, options...)
}
@@ -671,8 +688,8 @@ func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) {
return
}
func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
return errDriverNotImplemented
func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
return info, errDriverNotImplemented
}
func (wd *wdaDriver) LogoutNoneUI(packageName string) error {
@@ -742,11 +759,13 @@ func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) {
// [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.session.ID, "/screenshot"); err != nil {
return nil, errors.Wrap(err, "get WDA screenshot data failed")
return nil, errors.Wrap(code.DeviceScreenShotError,
fmt.Sprintf("get WDA screenshot data failed: %v", err))
}
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
return nil, errors.Wrap(err, "decode WDA screenshot data failed")
return nil, errors.Wrap(code.DeviceScreenShotError,
fmt.Sprintf("decode WDA screenshot data failed: %v", err))
}
return
}
@@ -864,6 +883,70 @@ func (wd *wdaDriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
return wd.httpPOST(data, "/gtf/automation/log")
}
func (wd *wdaDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) {
// 获取当前时间戳
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
// 创建文件名
fileName := fmt.Sprintf("%s/%s.mp4", folderPath, timestamp)
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
log.Error().Err(err).Msg("Error creating directory")
}
// 创建一个文件
file, err := os.Create(fileName)
if err != nil {
fmt.Println("Error creating file:", err)
return "", err
}
defer func() {
// 确保文件在程序结束时被删除
_ = file.Close()
}()
// ffmpeg 命令
cmd := exec.Command(
"ffmpeg",
"-use_wallclock_as_timestamps", "1",
"-f", "mjpeg",
"-y",
"-r", "10",
"-i", "http://"+wd.mjpegUrl,
"-c:v", "libx264",
"-vf", "pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2",
fileName,
)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
// 启动命令
if err := cmd.Start(); err != nil {
fmt.Println("Error starting ffmpeg command:", err)
return "", err
}
timer := time.After(duration)
done := make(chan error)
go func() {
// 等待 ffmpeg 命令执行完毕
done <- cmd.Wait()
}()
select {
case <-timer:
// 超时,停止 ffmpeg 进程
fmt.Println("Time is up, stopping ffmpeg command...")
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
fmt.Println("Error killing ffmpeg process:", err)
}
case err := <-done:
// ffmpeg 正常结束
if err != nil {
fmt.Println("FFmpeg finished with error:", err)
} else {
fmt.Println("FFmpeg finished successfully")
}
}
return filepath.Abs(fileName)
}
func (wd *wdaDriver) StartCaptureLog(identifier ...string) error {
log.Info().Msg("start WDA log recording")
if identifier == nil {
@@ -903,8 +986,20 @@ func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) {
return reply.Value, nil
}
func (ud *wdaDriver) GetSession() *DriverSession {
return &ud.Driver.session
func (wd *wdaDriver) GetSession() *DriverSession {
return &wd.Driver.session
}
func (wd *wdaDriver) GetDriverResults() []*DriverResult {
defer func() {
wd.Driver.driverResults = nil
}()
return wd.Driver.driverResults
}
func (wd *wdaDriver) TearDown() {
wd.mjpegClient.CloseIdleConnections()
wd.client.CloseIdleConnections()
}
type rawResponse []byte

View File

@@ -0,0 +1,569 @@
package uixt
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
)
type stubIOSDriver struct {
bightInsightPrefix string
serverPrefix string
timeout time.Duration
Driver
*wdaDriver
device *IOSDevice
}
func newStubIOSDriver(bightInsightAddr, serverAddr string, dev *IOSDevice, readTimeout ...time.Duration) (*stubIOSDriver, error) {
timeout := 10 * time.Second
if len(readTimeout) > 0 {
timeout = readTimeout[0]
}
driver := new(stubIOSDriver)
driver.device = dev
driver.bightInsightPrefix = bightInsightAddr
driver.serverPrefix = serverAddr
driver.timeout = timeout
driver.Driver.client = &http.Client{
Timeout: time.Second * 20, // 设置超时时间为 10 秒
}
return driver, nil
}
func (s *stubIOSDriver) setUpWda() (err error) {
if s.wdaDriver == nil {
capabilities := NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
driver, err := s.device.NewHTTPDriver(capabilities)
if err != nil {
log.Error().Err(err).Msg("stub driver failed to init wda driver")
return err
}
s.wdaDriver = driver.(*wdaDriver)
}
return nil
}
// NewSession starts a new session and returns the SessionInfo.
func (s *stubIOSDriver) NewSession(capabilities Capabilities) (SessionInfo, error) {
err := s.setUpWda()
if err != nil {
return SessionInfo{}, err
}
return s.wdaDriver.NewSession(capabilities)
}
// DeleteSession Kills application associated with that session and removes session
// 1. alertsMonitor disable
// 2. testedApplicationBundleId terminate
func (s *stubIOSDriver) DeleteSession() error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.DeleteSession()
}
func (s *stubIOSDriver) Status() (DeviceStatus, error) {
err := s.setUpWda()
if err != nil {
return DeviceStatus{}, err
}
return s.wdaDriver.Status()
}
func (s *stubIOSDriver) DeviceInfo() (DeviceInfo, error) {
err := s.setUpWda()
if err != nil {
return DeviceInfo{}, err
}
return s.wdaDriver.DeviceInfo()
}
func (s *stubIOSDriver) Location() (Location, error) {
err := s.setUpWda()
if err != nil {
return Location{}, err
}
return s.wdaDriver.Location()
}
func (s *stubIOSDriver) BatteryInfo() (BatteryInfo, error) {
err := s.setUpWda()
if err != nil {
return BatteryInfo{}, err
}
return s.wdaDriver.BatteryInfo()
}
// WindowSize Return the width and height in portrait mode.
// when getting the window size in wda/ui2/adb, if the device is in landscape mode,
// the width and height will be reversed.
func (s *stubIOSDriver) WindowSize() (Size, error) {
err := s.setUpWda()
if err != nil {
return Size{}, err
}
return s.wdaDriver.WindowSize()
}
func (s *stubIOSDriver) Screen() (Screen, error) {
err := s.setUpWda()
if err != nil {
return Screen{}, err
}
return s.wdaDriver.Screen()
}
func (s *stubIOSDriver) Scale() (float64, error) {
err := s.setUpWda()
if err != nil {
return 0, err
}
return s.wdaDriver.Scale()
}
// GetTimestamp returns the timestamp of the mobile device
func (s *stubIOSDriver) GetTimestamp() (timestamp int64, err error) {
err = s.setUpWda()
if err != nil {
return 0, err
}
return s.wdaDriver.GetTimestamp()
}
// Homescreen Forces the device under test to switch to the home screen
func (s *stubIOSDriver) Homescreen() error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Homescreen()
}
func (s *stubIOSDriver) Unlock() (err error) {
err = s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Unlock()
}
// AppLaunch Launch an application with given bundle identifier in scope of current session.
// !This method is only available since Xcode9 SDK
func (s *stubIOSDriver) AppLaunch(packageName string) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.AppLaunch(packageName)
}
// AppTerminate Terminate an application with the given package name.
// Either `true` if the app has been successfully terminated or `false` if it was not running
func (s *stubIOSDriver) AppTerminate(packageName string) (bool, error) {
err := s.setUpWda()
if err != nil {
return false, err
}
return s.wdaDriver.AppTerminate(packageName)
}
// GetForegroundApp returns current foreground app package name and activity name
func (s *stubIOSDriver) GetForegroundApp() (app AppInfo, err error) {
err = s.setUpWda()
if err != nil {
return AppInfo{}, err
}
return s.wdaDriver.GetForegroundApp()
}
// AssertForegroundApp returns nil if the given package and activity are in foreground
func (s *stubIOSDriver) AssertForegroundApp(packageName string, activityType ...string) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.AssertForegroundApp(packageName, activityType...)
}
// StartCamera Starts a new camera for recording
func (s *stubIOSDriver) StartCamera() error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.StartCamera()
}
// StopCamera Stops the camera for recording
func (s *stubIOSDriver) StopCamera() error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.StopCamera()
}
func (s *stubIOSDriver) Orientation() (orientation Orientation, err error) {
err = s.setUpWda()
if err != nil {
return OrientationPortrait, err
}
return s.wdaDriver.Orientation()
}
// Tap Sends a tap event at the coordinate.
func (s *stubIOSDriver) Tap(x, y int, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Tap(x, y, options...)
}
func (s *stubIOSDriver) TapFloat(x, y float64, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.TapFloat(x, y, options...)
}
// DoubleTap Sends a double tap event at the coordinate.
func (s *stubIOSDriver) DoubleTap(x, y float64, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.DoubleTap(x, y, options...)
}
// TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration.
//
// second: The default value is 1
func (s *stubIOSDriver) TouchAndHold(x, y float64, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.TouchAndHold(x, y, options...)
}
// Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate.
// WithPressDurationOption option can be used to set pressForDuration (default to 1 second).
func (s *stubIOSDriver) Drag(fromX, fromY, toX, toY float64, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Drag(fromX, fromY, toX, toY, options...)
}
// Swipe works like Drag, but `pressForDuration` value is 0
func (s *stubIOSDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Swipe(fromX, fromY, toX, toY, options...)
}
func (s *stubIOSDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.SwipeFloat(fromX, fromY, toX, toY, options...)
}
// SetPasteboard Sets data to the general pasteboard
func (s *stubIOSDriver) SetPasteboard(contentType PasteboardType, content string) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.SetPasteboard(contentType, content)
}
// GetPasteboard Gets the data contained in the general pasteboard.
//
// It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330
func (s *stubIOSDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) {
err = s.setUpWda()
if err != nil {
return nil, err
}
return s.wdaDriver.GetPasteboard(contentType)
}
func (s *stubIOSDriver) SetIme(ime string) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.SetIme(ime)
}
// SendKeys Types a string into active element. There must be element with keyboard focus,
// otherwise an error is raised.
// WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60
func (s *stubIOSDriver) SendKeys(text string, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.SendKeys(text, options...)
}
// Input works like SendKeys
func (s *stubIOSDriver) Input(text string, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Input(text, options...)
}
func (s *stubIOSDriver) Clear(packageName string) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.Clear(packageName)
}
// PressButton Presses the corresponding hardware button on the device
func (s *stubIOSDriver) PressButton(devBtn DeviceButton) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.PressButton(devBtn)
}
// PressBack Presses the back button
func (s *stubIOSDriver) PressBack(options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.PressBack(options...)
}
func (s *stubIOSDriver) PressKeyCode(keyCode KeyCode) (err error) {
err = s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.PressKeyCode(keyCode)
}
func (s *stubIOSDriver) Screenshot() (*bytes.Buffer, error) {
err := s.setUpWda()
if err != nil {
return nil, err
}
return s.wdaDriver.Screenshot()
//screenshotService, err := instruments.NewScreenshotService(s.device.d)
//if err != nil {
// log.Error().Err(err).Msg("Starting screenshot service failed")
// return nil, err
//}
//defer screenshotService.Close()
//
//imageBytes, err := screenshotService.TakeScreenshot()
//if err != nil {
// log.Error().Err(err).Msg("failed to task screenshot")
// return nil, err
//}
//return bytes.NewBuffer(imageBytes), nil
}
func (s *stubIOSDriver) TapByText(text string, options ...ActionOption) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.TapByText(text, options...)
}
func (s *stubIOSDriver) TapByTexts(actions ...TapTextAction) error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.TapByTexts(actions...)
}
// AccessibleSource Return application elements accessibility tree
func (s *stubIOSDriver) AccessibleSource() (string, error) {
err := s.setUpWda()
if err != nil {
return "", err
}
return s.wdaDriver.AccessibleSource()
}
// HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions
//
// Checks health of XCTest by:
// 1) Querying application for some elements,
// 2) Triggering some device events.
func (s *stubIOSDriver) HealthCheck() error {
err := s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.HealthCheck()
}
func (s *stubIOSDriver) GetAppiumSettings() (map[string]interface{}, error) {
err := s.setUpWda()
if err != nil {
return nil, err
}
return s.wdaDriver.GetAppiumSettings()
}
func (s *stubIOSDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) {
err := s.setUpWda()
if err != nil {
return nil, err
}
return s.wdaDriver.SetAppiumSettings(settings)
}
func (s *stubIOSDriver) IsHealthy() (bool, error) {
err := s.setUpWda()
if err != nil {
return false, err
}
return s.wdaDriver.IsHealthy()
}
// triggers the log capture and returns the log entries
func (s *stubIOSDriver) StartCaptureLog(identifier ...string) (err error) {
err = s.setUpWda()
if err != nil {
return err
}
return s.wdaDriver.StartCaptureLog(identifier...)
}
func (s *stubIOSDriver) StopCaptureLog() (result interface{}, err error) {
err = s.setUpWda()
if err != nil {
return nil, err
}
return s.wdaDriver.StopCaptureLog()
}
func (s *stubIOSDriver) GetDriverResults() []*DriverResult {
err := s.setUpWda()
if err != nil {
return nil
}
return s.wdaDriver.GetDriverResults()
}
func (s *stubIOSDriver) Source(srcOpt ...SourceOption) (string, error) {
resp, err := s.Driver.httpRequest(http.MethodGet, fmt.Sprintf("%s/source?format=json&onlyWeb=false", s.bightInsightPrefix), []byte{})
if err != nil {
return "", err
}
return string(resp), nil
}
func (s *stubIOSDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) {
params := map[string]interface{}{
"phone": phoneNumber,
}
if captcha != "" {
params["captcha"] = captcha
} else if password != "" {
params["password"] = password
} else {
return info, fmt.Errorf("password and capcha is empty")
}
bsJSON, err := json.Marshal(params)
if err != nil {
return info, err
}
resp, err := s.Driver.httpRequest(http.MethodPost, fmt.Sprintf("%s/host/login/account/", s.serverPrefix), bsJSON)
if err != nil {
return info, err
}
res, err := resp.valueConvertToJsonObject()
if err != nil {
return info, err
}
log.Info().Msgf("%v", res)
// {'isSuccess': True, 'data': '登录成功', 'code': 0}
if res["isSuccess"] != true {
err = fmt.Errorf("falied to logout %s", res["data"])
log.Err(err).Msgf("%v", res)
return info, err
}
time.Sleep(20 * time.Second)
info, err = s.getLoginAppInfo(packageName)
if err != nil || !info.IsLogin {
return info, fmt.Errorf("falied to login %v", info)
}
return info, nil
}
func (s *stubIOSDriver) LogoutNoneUI(packageName string) error {
resp, err := s.Driver.httpRequest(http.MethodGet, fmt.Sprintf("%s/host/loginout/", s.serverPrefix), []byte{})
if err != nil {
return err
}
res, err := resp.valueConvertToJsonObject()
if err != nil {
return err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("falied to logout %s", res["data"])
log.Err(err).Msgf("%v", res)
return err
}
time.Sleep(10 * time.Second)
return nil
}
func (s *stubIOSDriver) TearDown() {
s.Driver.client.CloseIdleConnections()
return
}
func (s *stubIOSDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) {
resp, err := s.Driver.httpRequest(http.MethodGet, fmt.Sprintf("%s/host/app/info/", s.serverPrefix), []byte{})
if err != nil {
return info, err
}
res, err := resp.valueConvertToJsonObject()
if err != nil {
return info, err
}
log.Info().Msgf("%v", res)
if res["isSuccess"] != true {
err = fmt.Errorf("falied to get is login %s", res["data"])
log.Err(err).Msgf("%v", res)
return info, err
}
err = json.Unmarshal([]byte(res["data"].(string)), &info)
if err != nil {
return info, err
}
return info, nil
}
func (s *stubIOSDriver) GetSession() *DriverSession {
return &s.Driver.session
}

View File

@@ -0,0 +1,105 @@
package uixt
import (
"fmt"
"net"
"os"
"testing"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
var (
iOSStubDriver IWebDriver
iOSDevice *IOSDevice
)
func setupiOSStubDriver(t *testing.T) {
var err error
iOSDevice, err = NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800), WithIOSStub(false))
checkErr(t, err)
iOSStubDriver, err = iOSDevice.NewStubDriver()
checkErr(t, err)
}
func TestIOSLogin(t *testing.T) {
setupiOSStubDriver(t)
info, err := iOSStubDriver.LoginNoneUI("", "12342316231", "8517", "")
checkErr(t, err)
t.Log(info)
}
func TestIOSLogout(t *testing.T) {
setupiOSStubDriver(t)
err := iOSStubDriver.LogoutNoneUI("")
checkErr(t, err)
}
func TestIOSIsLogin(t *testing.T) {
setupiOSStubDriver(t)
err := iOSStubDriver.LogoutNoneUI("")
checkErr(t, err)
}
func TestIOSSource(t *testing.T) {
setupiOSStubDriver(t)
source, err := iOSStubDriver.Source()
checkErr(t, err)
t.Log(source)
}
func TestIOSForeground(t *testing.T) {
setupiOSStubDriver(t)
app, err := iOSStubDriver.GetForegroundApp()
checkErr(t, err)
t.Log(app)
}
func TestIOSSwipe(t *testing.T) {
setupiOSStubDriver(t)
iOSStubDriver.Swipe(540, 0, 540, 1000)
}
func TestIOSSave(t *testing.T) {
setupiOSStubDriver(t)
raw, err := iOSStubDriver.Screenshot()
if err != nil {
t.Fatal(err)
}
source, err := iOSStubDriver.Source()
if err != nil {
t.Fatal(err)
}
step := 7
file, err := os.Create(fmt.Sprintf("/Users/bytedance/workcode/wings_algorithm/testcases/data/cases/ios/4159417_cvcn02okg4g0/%d.jpg", step))
if err != nil {
t.Fatal(err)
}
file.Write(raw.Bytes())
file, err = os.Create(fmt.Sprintf("/Users/bytedance/workcode/wings_algorithm/testcases/data/cases/ios/4159417_cvcn02okg4g0/%d.json", step))
if err != nil {
t.Fatal(err)
}
file.Write([]byte(source))
}
func TestListen(t *testing.T) {
setupiOSStubDriver(t)
localPort, err := builtin.GetFreePort()
if err != nil {
t.Fatal(err)
}
err = iOSDevice.forward(localPort, 8800)
if err != nil {
t.Fatal(err)
}
addr := fmt.Sprintf("0.0.0.0:%d", localPort)
l, err := net.Listen("tcp", addr)
if err == nil {
l.Close() // 端口成功绑定后立即释放,返回该端口号
} else {
t.Fatal(err)
}
}

View File

@@ -24,7 +24,7 @@ func setup(t *testing.T) {
}
capabilities := NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
driver, err = device.NewUSBDriver(capabilities)
driver, err = device.NewHTTPDriver(capabilities)
if err != nil {
t.Fatal(err)
}
@@ -49,7 +49,7 @@ func TestInstall(t *testing.T) {
}
func TestNewIOSDevice(t *testing.T) {
device, _ := NewIOSDevice()
device, _ := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800))
if device != nil {
t.Log(device)
}
@@ -70,8 +70,16 @@ func TestNewIOSDevice(t *testing.T) {
}
}
func TestIOSDevice_GetPackageInfo(t *testing.T) {
device, err := NewIOSDevice(WithWDAPort(8700))
checkErr(t, err)
appInfo, err := device.GetPackageInfo("com.apple.Preferences")
checkErr(t, err)
t.Log(appInfo)
}
func TestNewWDAHTTPDriver(t *testing.T) {
device, _ := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800))
device, _ := NewIOSDevice()
var err error
_, err = device.NewHTTPDriver(nil)
if err != nil {
@@ -85,14 +93,6 @@ func TestNewUSBDriver(t *testing.T) {
// t.Log(driver.IsWdaHealthy())
}
func TestIOSDevice_GetPackageInfo(t *testing.T) {
device, err := NewIOSDevice(WithWDAPort(8700))
checkErr(t, err)
appInfo, err := device.GetPackageInfo("com.apple.Preferences")
checkErr(t, err)
t.Log(appInfo)
}
func TestDriver_DeviceScaleRatio(t *testing.T) {
setup(t)
@@ -276,7 +276,7 @@ func Test_remoteWD_TouchAndHold(t *testing.T) {
setup(t)
// err := driver.TouchAndHold(200, 300)
err := driver.TouchAndHold(200, 300, -1)
err := driver.TouchAndHold(200, 300)
if err != nil {
t.Fatal(err)
}
@@ -286,7 +286,7 @@ func Test_remoteWD_Drag(t *testing.T) {
setup(t)
// err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5))
err := driver.Swipe(200, 300, 200, 500)
err := driver.Drag(200, 300, 200, 500, WithPressDuration(2), WithDuration(3))
if err != nil {
t.Fatal(err)
}
@@ -308,7 +308,7 @@ func Test_remoteWD_SetPasteboard(t *testing.T) {
// err := driver.SetPasteboard(PasteboardTypePlaintext, "gwda")
err := driver.SetPasteboard(PasteboardTypeUrl, "Clock-stopwatch://")
// userHomeDir, _ := os.UserHomeDir()
// bytesImg, _ := os.ReadFile(userHomeDir + "/Pictures/IMG_0806.jpg")
// bytesImg, _ := ioutil.ReadFile(userHomeDir + "/Pictures/IMG_0806.jpg")
// err := driver.SetPasteboard(PasteboardTypeImage, string(bytesImg))
if err != nil {
t.Fatal(err)
@@ -333,21 +333,21 @@ func Test_remoteWD_GetPasteboard(t *testing.T) {
// t.Fatal(err)
// }
// userHomeDir, _ := os.UserHomeDir()
// if err = os.WriteFile(userHomeDir+"/Desktop/p1.png", buffer.Bytes(), 0600); err != nil {
// if err = ioutil.WriteFile(userHomeDir+"/Desktop/p1.png", buffer.Bytes(), 0600); err != nil {
// t.Error(err)
// }
}
func Test_remoteWD_SendKeys(t *testing.T) {
setup(t)
driver.StartCaptureLog("hrp_wda_log")
err := driver.SendKeys("", WithIdentifier("test"))
result, _ := driver.StopCaptureLog()
// driver.StartCaptureLog("hrp_wda_log")
err := driver.SendKeys("test", WithIdentifier("test"))
// result, _ := driver.StopCaptureLog()
// err := driver.SendKeys("App Store", WithFrequency(3))
if err != nil {
t.Fatal(err)
}
t.Log(result)
// t.Log(result)
}
func Test_remoteWD_PressButton(t *testing.T) {
@@ -445,3 +445,21 @@ func Test_remoteWD_AccessibleSource(t *testing.T) {
_ = source
fmt.Println(source)
}
func TestRecord(t *testing.T) {
setup(t)
path, err := driver.(*wdaDriver).RecordScreen("", 5*time.Second)
if err != nil {
t.Fatal(err)
}
println(path)
}
// func Test_Backspace(t *testing.T) {
// setup(t)
// err := driver.Backspace(3)
// if err != nil {
// t.Fatal(err)
// }
// }

View File

@@ -93,7 +93,7 @@ func (dExt *DriverExt) DoubleTapXY(x, y float64, options ...ActionOption) error
}
x = x * float64(windowSize.Width)
y = y * float64(windowSize.Height)
err = dExt.Driver.DoubleTapFloat(x, y, options...)
err = dExt.Driver.DoubleTap(x, y, options...)
if err != nil {
return errors.Wrap(code.MobileUITapError, err.Error())
}
@@ -110,7 +110,7 @@ func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64, o
return err
}
err = dExt.Driver.DoubleTapFloat(point.X+xOffset, point.Y+yOffset, options...)
err = dExt.Driver.DoubleTap(point.X+xOffset, point.Y+yOffset, options...)
if err != nil {
return errors.Wrap(code.MobileUITapError, err.Error())
}