mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-01 13:59:37 +08:00
refactor: NewAndroidDevice
This commit is contained in:
2
go.mod
2
go.mod
@@ -2,8 +2,6 @@ module github.com/httprunner/httprunner/v5
|
|||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
toolchain go1.22.7
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27
|
code.byted.org/iesqa/ghdc v0.0.0-20241009025217-ecb76cf5bd27
|
||||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
|
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v5.0.0+2502071620
|
v5.0.0+2502071622
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// adb server
|
|
||||||
AdbServerHost = "localhost"
|
|
||||||
AdbServerPort = gadb.AdbServerPort // 5037
|
|
||||||
|
|
||||||
EvalInstallerPackageName = "sogou.mobile.explorer"
|
EvalInstallerPackageName = "sogou.mobile.explorer"
|
||||||
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
|
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
|
||||||
)
|
)
|
||||||
@@ -42,76 +38,54 @@ var evalite embed.FS
|
|||||||
func NewAndroidDevice(opts ...option.AndroidDeviceOption) (device *AndroidDevice, err error) {
|
func NewAndroidDevice(opts ...option.AndroidDeviceOption) (device *AndroidDevice, err error) {
|
||||||
androidOptions := option.NewAndroidDeviceOptions(opts...)
|
androidOptions := option.NewAndroidDeviceOptions(opts...)
|
||||||
|
|
||||||
deviceList, err := GetAndroidDevices(androidOptions.SerialNumber)
|
// get all attached android devices
|
||||||
|
adbClient, err := gadb.NewClientWith(
|
||||||
|
androidOptions.AdbServerHost, androidOptions.AdbServerPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
|
return nil, err
|
||||||
|
}
|
||||||
|
devices, err := adbClient.DeviceList()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return nil, errors.Wrapf(code.DeviceConnectionError,
|
||||||
|
"no attached android devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
if androidOptions.SerialNumber == "" && len(deviceList) > 1 {
|
// filter device by serial
|
||||||
return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the serial")
|
var gadbDevice *gadb.Device
|
||||||
}
|
|
||||||
|
|
||||||
dev := deviceList[0]
|
|
||||||
|
|
||||||
if androidOptions.SerialNumber == "" {
|
if androidOptions.SerialNumber == "" {
|
||||||
selectSerial := dev.Serial()
|
if len(devices) > 1 {
|
||||||
androidOptions.SerialNumber = selectSerial
|
return nil, errors.Wrap(code.DeviceConnectionError,
|
||||||
log.Warn().
|
"more than one device connected, please specify the serial")
|
||||||
Str("serial", androidOptions.SerialNumber).
|
}
|
||||||
Msg("android SerialNumber is not specified, select the first one")
|
gadbDevice = devices[0]
|
||||||
|
androidOptions.SerialNumber = gadbDevice.Serial()
|
||||||
|
log.Warn().Str("serial", androidOptions.SerialNumber).
|
||||||
|
Msg("android SerialNumber is not specified, select the attached one")
|
||||||
|
} else {
|
||||||
|
for _, d := range devices {
|
||||||
|
if d.Serial() == androidOptions.SerialNumber {
|
||||||
|
gadbDevice = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gadbDevice == nil {
|
||||||
|
return nil, errors.Wrapf(code.DeviceConnectionError,
|
||||||
|
"android device %s not attached", androidOptions.SerialNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
device = &AndroidDevice{
|
device = &AndroidDevice{
|
||||||
Device: dev,
|
Device: gadbDevice,
|
||||||
AndroidDeviceOptions: androidOptions,
|
AndroidDeviceOptions: androidOptions,
|
||||||
Logcat: NewAdbLogcat(androidOptions.SerialNumber),
|
Logcat: NewAdbLogcat(androidOptions.SerialNumber),
|
||||||
}
|
}
|
||||||
|
|
||||||
evalToolRaw, err := evalite.ReadFile("evalite")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.LoadFileError, err.Error())
|
|
||||||
}
|
|
||||||
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.DeviceShellExecError, err.Error())
|
|
||||||
}
|
|
||||||
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
|
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
|
||||||
return device, nil
|
return device, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
|
|
||||||
var adbClient gadb.Client
|
|
||||||
if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if devices, err = adbClient.DeviceList(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceList []*gadb.Device
|
|
||||||
// filter by serial
|
|
||||||
for _, d := range devices {
|
|
||||||
for _, s := range serial {
|
|
||||||
if s != "" && s != d.Serial() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
deviceList = append(deviceList, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(deviceList) == 0 {
|
|
||||||
var err error
|
|
||||||
if serial == nil || (len(serial) == 1 && serial[0] == "") {
|
|
||||||
err = fmt.Errorf("no android device found")
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("no android device found for serial %v", serial)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return deviceList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AndroidDevice struct {
|
type AndroidDevice struct {
|
||||||
*gadb.Device
|
*gadb.Device
|
||||||
*option.AndroidDeviceOptions
|
*option.AndroidDeviceOptions
|
||||||
@@ -122,6 +96,16 @@ func (dev *AndroidDevice) Setup() error {
|
|||||||
dev.RunShellCommand("ime", "enable", UnicodeImePackageName)
|
dev.RunShellCommand("ime", "enable", UnicodeImePackageName)
|
||||||
dev.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath)
|
dev.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath)
|
||||||
|
|
||||||
|
// setup evalite
|
||||||
|
evalToolRaw, err := evalite.ReadFile("evalite")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(code.LoadFileError, err.Error())
|
||||||
|
}
|
||||||
|
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(code.DeviceShellExecError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if dev.UIA2 {
|
if dev.UIA2 {
|
||||||
// uiautomator2 server must be started before
|
// uiautomator2 server must be started before
|
||||||
|
|
||||||
@@ -211,11 +195,6 @@ func (dev *AndroidDevice) StopPcap() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dev *AndroidDevice) Uninstall(packageName string) error {
|
|
||||||
_, err := dev.RunShellCommand("uninstall", packageName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dev *AndroidDevice) Install(apkPath string, opts ...option.InstallOption) error {
|
func (dev *AndroidDevice) Install(apkPath string, opts ...option.InstallOption) error {
|
||||||
installOpts := option.NewInstallOptions(opts...)
|
installOpts := option.NewInstallOptions(opts...)
|
||||||
brand, err := dev.Brand()
|
brand, err := dev.Brand()
|
||||||
@@ -317,6 +296,11 @@ func (dev *AndroidDevice) installCommon(apkPath string, args ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dev *AndroidDevice) Uninstall(packageName string) error {
|
||||||
|
_, err := dev.RunShellCommand("uninstall", packageName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) {
|
func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) {
|
||||||
// adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
|
// adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
|
||||||
output, err := dev.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
|
output, err := dev.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
|
||||||
@@ -618,346 +602,3 @@ func ConvertPoints(lines []string) (eps []ExportPoint) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type UiSelectorHelper struct {
|
|
||||||
value *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUiSelectorHelper() UiSelectorHelper {
|
|
||||||
return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s UiSelectorHelper) String() string {
|
|
||||||
return s.value.String() + ";"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text Set the search criteria to match the visible text displayed
|
|
||||||
// in a widget (for example, the text label to launch an app).
|
|
||||||
//
|
|
||||||
// The text for the element must match exactly with the string in your input
|
|
||||||
// argument. Matching is case-sensitive.
|
|
||||||
func (s UiSelectorHelper) Text(text string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.text("%s")`, text))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextMatches Set the search criteria to match the visible text displayed in a layout
|
|
||||||
// element, using a regular expression.
|
|
||||||
//
|
|
||||||
// The text in the widget must match exactly with the string in your
|
|
||||||
// input argument.
|
|
||||||
func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextStartsWith Set the search criteria to match visible text in a widget that is
|
|
||||||
// prefixed by the text parameter.
|
|
||||||
//
|
|
||||||
// The matching is case-insensitive.
|
|
||||||
func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextContains Set the search criteria to match the visible text in a widget
|
|
||||||
// where the visible text must contain the string in your input argument.
|
|
||||||
//
|
|
||||||
// The matching is case-sensitive.
|
|
||||||
func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClassName Set the search criteria to match the class property
|
|
||||||
// for a widget (for example, "android.widget.Button").
|
|
||||||
func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.className("%s")`, className))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClassNameMatches Set the search criteria to match the class property
|
|
||||||
// for a widget, using a regular expression.
|
|
||||||
func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description Set the search criteria to match the content-description
|
|
||||||
// property for a widget.
|
|
||||||
//
|
|
||||||
// The content-description is typically used
|
|
||||||
// by the Android Accessibility framework to
|
|
||||||
// provide an audio prompt for the widget when
|
|
||||||
// the widget is selected. The content-description
|
|
||||||
// for the widget must match exactly
|
|
||||||
// with the string in your input argument.
|
|
||||||
//
|
|
||||||
// Matching is case-sensitive.
|
|
||||||
func (s UiSelectorHelper) Description(desc string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// DescriptionMatches Set the search criteria to match the content-description
|
|
||||||
// property for a widget.
|
|
||||||
//
|
|
||||||
// The content-description is typically used
|
|
||||||
// by the Android Accessibility framework to
|
|
||||||
// provide an audio prompt for the widget when
|
|
||||||
// the widget is selected. The content-description
|
|
||||||
// for the widget must match exactly
|
|
||||||
// with the string in your input argument.
|
|
||||||
func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// DescriptionStartsWith Set the search criteria to match the content-description
|
|
||||||
// property for a widget.
|
|
||||||
//
|
|
||||||
// The content-description is typically used
|
|
||||||
// by the Android Accessibility framework to
|
|
||||||
// provide an audio prompt for the widget when
|
|
||||||
// the widget is selected. The content-description
|
|
||||||
// for the widget must start
|
|
||||||
// with the string in your input argument.
|
|
||||||
//
|
|
||||||
// Matching is case-insensitive.
|
|
||||||
func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// DescriptionContains Set the search criteria to match the content-description
|
|
||||||
// property for a widget.
|
|
||||||
//
|
|
||||||
// The content-description is typically used
|
|
||||||
// by the Android Accessibility framework to
|
|
||||||
// provide an audio prompt for the widget when
|
|
||||||
// the widget is selected. The content-description
|
|
||||||
// for the widget must contain
|
|
||||||
// the string in your input argument.
|
|
||||||
//
|
|
||||||
// Matching is case-insensitive.
|
|
||||||
func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResourceId Set the search criteria to match the given resource ID.
|
|
||||||
func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResourceIdMatches Set the search criteria to match the resource ID
|
|
||||||
// of the widget, using a regular expression.
|
|
||||||
func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index Set the search criteria to match the widget by its node
|
|
||||||
// index in the layout hierarchy.
|
|
||||||
//
|
|
||||||
// The index value must be 0 or greater.
|
|
||||||
//
|
|
||||||
// Using the index can be unreliable and should only
|
|
||||||
// be used as a last resort for matching. Instead,
|
|
||||||
// consider using the `Instance(int)` method.
|
|
||||||
func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.index(%d)`, index))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instance Set the search criteria to match the
|
|
||||||
// widget by its instance number.
|
|
||||||
//
|
|
||||||
// The instance value must be 0 or greater, where
|
|
||||||
// the first instance is 0.
|
|
||||||
//
|
|
||||||
// For example, to simulate a user click on
|
|
||||||
// the third image that is enabled in a UI screen, you
|
|
||||||
// could specify a search criteria where the instance is
|
|
||||||
// 2, the `className(String)` matches the image
|
|
||||||
// widget class, and `enabled(boolean)` is true.
|
|
||||||
// The code would look like this:
|
|
||||||
//
|
|
||||||
// `new UiSelector().className("android.widget.ImageView")
|
|
||||||
// .enabled(true).instance(2);`
|
|
||||||
func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled Set the search criteria to match widgets that are enabled.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focused Set the search criteria to match widgets that have focus.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focusable Set the search criteria to match widgets that are focusable.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scrollable Set the search criteria to match widgets that are scrollable.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected Set the search criteria to match widgets that
|
|
||||||
// are currently selected.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checked Set the search criteria to match widgets that
|
|
||||||
// are currently checked (usually for checkboxes).
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checkable Set the search criteria to match widgets that are checkable.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clickable Set the search criteria to match widgets that are clickable.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// LongClickable Set the search criteria to match widgets that are long-clickable.
|
|
||||||
//
|
|
||||||
// Typically, using this search criteria alone is not useful.
|
|
||||||
// You should also include additional criteria, such as text,
|
|
||||||
// content-description, or the class name for a widget.
|
|
||||||
//
|
|
||||||
// If no other search criteria is specified, and there is more
|
|
||||||
// than one matching widget, the first widget in the tree
|
|
||||||
// is selected.
|
|
||||||
func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// packageName Set the search criteria to match the package name
|
|
||||||
// of the application that contains the widget.
|
|
||||||
func (s UiSelectorHelper) packageName(name string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// PackageNameMatches Set the search criteria to match the package name
|
|
||||||
// of the application that contains the widget.
|
|
||||||
func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChildSelector Adds a child UiSelector criteria to this selector.
|
|
||||||
//
|
|
||||||
// Use this selector to narrow the search scope to
|
|
||||||
// child widgets under a specific parent widget.
|
|
||||||
func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String()))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String()))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String()))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromParent Adds a child UiSelector criteria to this selector which is used to
|
|
||||||
// start search from the parent widget.
|
|
||||||
//
|
|
||||||
// Use this selector to narrow the search scope to
|
|
||||||
// sibling widgets as well all child widgets under a parent.
|
|
||||||
func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper {
|
|
||||||
s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String()))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -236,33 +236,6 @@ func TestDriver_GetOrientation(t *testing.T) {
|
|||||||
_ = driverExt.Driver.Homescreen()
|
_ = driverExt.Driver.Homescreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) {
|
|
||||||
uiSelector := NewUiSelectorHelper().Text("a").String()
|
|
||||||
if uiSelector != `new UiSelector().text("a");` {
|
|
||||||
t.Fatal("[ERROR]", uiSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiSelector = NewUiSelectorHelper().Text("a").TextStartsWith("b").String()
|
|
||||||
if uiSelector != `new UiSelector().text("a").textStartsWith("b");` {
|
|
||||||
t.Fatal("[ERROR]", uiSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiSelector = NewUiSelectorHelper().ClassName("android.widget.LinearLayout").Index(6).String()
|
|
||||||
if uiSelector != `new UiSelector().className("android.widget.LinearLayout").index(6);` {
|
|
||||||
t.Fatal("[ERROR]", uiSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiSelector = NewUiSelectorHelper().Focused(false).Instance(6).String()
|
|
||||||
if uiSelector != `new UiSelector().focused(false).instance(6);` {
|
|
||||||
t.Fatal("[ERROR]", uiSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiSelector = NewUiSelectorHelper().ChildSelector(NewUiSelectorHelper().Enabled(true)).String()
|
|
||||||
if uiSelector != `new UiSelector().childSelector(new UiSelector().enabled(true));` {
|
|
||||||
t.Fatal("[ERROR]", uiSelector)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_getFreePort(t *testing.T) {
|
func Test_getFreePort(t *testing.T) {
|
||||||
freePort, err := builtin.GetFreePort()
|
freePort, err := builtin.GetFreePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -271,16 +244,6 @@ func Test_getFreePort(t *testing.T) {
|
|||||||
t.Log(freePort)
|
t.Log(freePort)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeviceList(t *testing.T) {
|
|
||||||
devices, err := GetAndroidDevices()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for i := range devices {
|
|
||||||
t.Log(devices[i].Serial())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDriver_AppLaunch(t *testing.T) {
|
func TestDriver_AppLaunch(t *testing.T) {
|
||||||
device, _ := NewAndroidDevice()
|
device, _ := NewAndroidDevice()
|
||||||
driver, err := device.NewDriver()
|
driver, err := device.NewDriver()
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
package uixt_ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/code"
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/json"
|
|
||||||
"github.com/httprunner/httprunner/v5/pkg/uixt"
|
|
||||||
"github.com/httprunner/httprunner/v5/pkg/uixt/option"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
StubSocketName = "com.bytest.device"
|
|
||||||
DouyinServerPort = 32316
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewStubDriver(device *uixt.AndroidDevice, opts ...option.DriverOption) (driver *StubAndroidDriver, err error) {
|
|
||||||
socketLocalPort, err := device.Forward(StubSocketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.DeviceConnectionError,
|
|
||||||
fmt.Sprintf("forward port %d->%s failed: %v",
|
|
||||||
socketLocalPort, StubSocketName, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
serverLocalPort, err := device.Forward(DouyinServerPort)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.DeviceConnectionError,
|
|
||||||
fmt.Sprintf("forward port %d->%d failed: %v",
|
|
||||||
serverLocalPort, DouyinServerPort, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
address := fmt.Sprintf("127.0.0.1:%d", socketLocalPort)
|
|
||||||
conn, err := net.Dial("tcp", address)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg(fmt.Sprintf("failed to connect %s", address))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
driver = &StubAndroidDriver{
|
|
||||||
socket: conn,
|
|
||||||
timeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
rawURL := fmt.Sprintf("http://forward-to-%d:%d",
|
|
||||||
serverLocalPort, DouyinServerPort)
|
|
||||||
if driver.urlPrefix, err = url.Parse(rawURL); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
driver.Device = device.Device
|
|
||||||
driver.Logcat = device.Logcat
|
|
||||||
return driver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StubAndroidDriver struct {
|
|
||||||
uixt.ADBDriver
|
|
||||||
socket net.Conn
|
|
||||||
seq int
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) {
|
|
||||||
var localPort int
|
|
||||||
{
|
|
||||||
tmpURL, _ := url.Parse(sad.urlPrefix.String())
|
|
||||||
hostname := tmpURL.Hostname()
|
|
||||||
if strings.HasPrefix(hostname, forwardToPrefix) {
|
|
||||||
localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("adb forward: %w", err)
|
|
||||||
}
|
|
||||||
sad.Client = convertToHTTPClient(conn)
|
|
||||||
return sad.Request(http.MethodGet, sad.concatURL(nil, pathElem...), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) {
|
|
||||||
var localPort int
|
|
||||||
{
|
|
||||||
tmpURL, _ := url.Parse(sad.urlPrefix.String())
|
|
||||||
hostname := tmpURL.Hostname()
|
|
||||||
if strings.HasPrefix(hostname, forwardToPrefix) {
|
|
||||||
localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("adb forward: %w", err)
|
|
||||||
}
|
|
||||||
sad.Client = convertToHTTPClient(conn)
|
|
||||||
|
|
||||||
var bsJSON []byte = nil
|
|
||||||
if data != nil {
|
|
||||||
if bsJSON, err = json.Marshal(data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sad.Request(http.MethodPost, sad.concatURL(nil, pathElem...), bsJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) sendCommand(packageName string, cmdType string, params map[string]interface{}, readTimeout ...time.Duration) (interface{}, error) {
|
|
||||||
sad.seq++
|
|
||||||
packet := map[string]interface{}{
|
|
||||||
"Seq": sad.seq,
|
|
||||||
"Cmd": cmdType,
|
|
||||||
"v": "",
|
|
||||||
}
|
|
||||||
for key, value := range params {
|
|
||||||
if key == "Cmd" || key == "Seq" {
|
|
||||||
return "", errors.New("params cannot be Cmd or Seq")
|
|
||||||
}
|
|
||||||
packet[key] = value
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(packet)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := sad.Device.RunStubCommand(append(data, '\n'), packageName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var resultMap map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(res), &resultMap); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resultMap["Error"] != nil {
|
|
||||||
return nil, fmt.Errorf("failed to call stub command: %s", resultMap["Error"].(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultMap["Result"], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) DeleteSession() error {
|
|
||||||
return sad.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) close() error {
|
|
||||||
if sad.socket != nil {
|
|
||||||
return sad.socket.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) Status() (uixt.DeviceStatus, error) {
|
|
||||||
app, err := sad.GetForegroundApp()
|
|
||||||
if err != nil {
|
|
||||||
return uixt.DeviceStatus{}, err
|
|
||||||
}
|
|
||||||
res, err := sad.sendCommand(app.PackageName, "Hello", nil)
|
|
||||||
if err != nil {
|
|
||||||
return uixt.DeviceStatus{}, err
|
|
||||||
}
|
|
||||||
log.Info().Msg(fmt.Sprintf("ping stub result :%v", res))
|
|
||||||
return uixt.DeviceStatus{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) Source(srcOpt ...option.SourceOption) (source string, err error) {
|
|
||||||
app, err := sad.GetForegroundApp()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
params := map[string]interface{}{
|
|
||||||
"ClassName": "com.bytedance.byteinsight.MockOperator",
|
|
||||||
"Method": "getLayout",
|
|
||||||
"RetType": "",
|
|
||||||
"Args": []string{},
|
|
||||||
}
|
|
||||||
res, err := sad.sendCommand(app.PackageName, "CallStaticMethod", params)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return res.(string), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) 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")
|
|
||||||
}
|
|
||||||
resp, err := sad.httpPOST(params, "/host", "/login", "account")
|
|
||||||
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 login %s", res["data"])
|
|
||||||
log.Err(err).Msgf("%v", res)
|
|
||||||
return info, 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)
|
|
||||||
}
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) LogoutNoneUI(packageName string) error {
|
|
||||||
resp, err := sad.httpGET("/host", "/logout")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
fmt.Printf("%v", resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) LoginNoneUIDynamic(packageName, phoneNumber string, captcha string) error {
|
|
||||||
params := map[string]interface{}{
|
|
||||||
"ClassName": "qe.python.test.LoginUtil",
|
|
||||||
"Method": "loginSync",
|
|
||||||
"RetType": "",
|
|
||||||
"Args": []string{phoneNumber, captcha},
|
|
||||||
}
|
|
||||||
res, err := sad.sendCommand(packageName, "CallStaticMethod", params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info().Msg(res.(string))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sad *StubAndroidDriver) SetHDTStatus(status bool) error {
|
|
||||||
_, err := sad.Device.RunShellCommand("settings", "put", "global", "feedbacker_sso_bypass_token", "default_sso_bypass_token")
|
|
||||||
if err != nil {
|
|
||||||
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 info, err
|
|
||||||
}
|
|
||||||
if res["isSuccess"] != true {
|
|
||||||
err = fmt.Errorf("falied to get app info %s", res["data"])
|
|
||||||
log.Err(err).Msgf("%v", res)
|
|
||||||
return info, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertToHTTPClient(conn net.Conn) *http.Client {
|
|
||||||
return &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return conn, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user