feat: 新增按控件点击,获取设备应用,修改日志获取

This commit is contained in:
余泓铮
2024-04-25 15:02:38 +08:00
parent d3aa72a713
commit d7d6f76c93
10 changed files with 345 additions and 90 deletions

View File

@@ -483,3 +483,10 @@ func ConvertToStringSlice(val interface{}) ([]string, error) {
}
return nil, fmt.Errorf("invalid type for conversion to []string")
}
func GetCurrentDay() string {
now := time.Now()
// 格式化日期为 yyyyMMdd
formattedDate := now.Format("20060102")
return formattedDate
}

View File

@@ -107,25 +107,38 @@ func (d *Device) features() (features Features, err error) {
return features, nil
}
func (d Device) HasAttribute(key string) bool {
func (d *Device) HasAttribute(key string) bool {
_, ok := d.attrs[key]
return ok
}
func (d Device) Product() (string, error) {
func (d *Device) Product() (string, error) {
if d.HasAttribute("product") {
return d.attrs["product"], nil
}
return "", errors.New("does not have attribute: product")
}
func (d Device) Model() (string, error) {
func (d *Device) Model() (string, error) {
if d.HasAttribute("model") {
return d.attrs["model"], nil
}
return "", errors.New("does not have attribute: model")
}
func (d *Device) Brand() (string, error) {
if d.HasAttribute("brand") {
return d.attrs["brand"], nil
}
brand, err := d.RunShellCommand("getprop", "ro.product.brand")
brand = strings.TrimSpace(brand)
if err != nil {
return "", errors.New("does not have attribute: brand")
}
d.attrs["brand"] = brand
return brand, nil
}
func (d *Device) Usb() (string, error) {
if d.HasAttribute("usb") {
return d.attrs["usb"], nil
@@ -133,7 +146,7 @@ func (d *Device) Usb() (string, error) {
return "", errors.New("does not have attribute: usb")
}
func (d Device) transportId() (string, error) {
func (d *Device) transportId() (string, error) {
if d.HasAttribute("transport_id") {
return d.attrs["transport_id"], nil
}
@@ -524,7 +537,7 @@ func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
return
}
func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byte, err error) {
var (
tp transport
filesize int64
@@ -537,8 +550,11 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
return nil, err
}
defer func() { _ = tp.Close() }()
cmd := fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)
cmd := "abb_exec:package\x00install\x00-t"
for _, arg := range args {
cmd += "\x00" + arg
}
cmd += fmt.Sprintf("\x00-S\x00%d", filesize)
if err = tp.SendWithCheck(cmd); err != nil {
return nil, err
}
@@ -555,7 +571,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
return
}
func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
func (d *Device) InstallAPK(apk io.ReadSeeker, args ...string) (string, error) {
haserr := func(ret string) bool {
return strings.Contains(ret, "Failure")
}
@@ -575,8 +591,9 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
if err != nil {
return "", fmt.Errorf("error pushing: %v", err)
}
res, err := d.RunShellCommand("pm", "install", "-f", remote)
args = append([]string{"install"}, args...)
args = append(args, "-f", remote)
res, err := d.RunShellCommand("pm", args...)
if err != nil {
return "", errors.Wrap(err, "install apk failed")
}
@@ -603,6 +620,33 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
return d.RunShellCommand("pm", args...)
}
func (d *Device) ListPackages() ([]string, error) {
args := []string{"list", "packages"}
resRaw, err := d.RunShellCommand("pm", args...)
if err != nil {
return []string{}, err
}
lines := strings.Split(resRaw, "\n")
var packages []string
for _, line := range lines {
packageName := strings.TrimPrefix(line, "package:")
packages = append(packages, packageName)
}
return packages, nil
}
func (d *Device) IsPackagesInstalled(packageName string) bool {
packages, err := d.ListPackages()
if err != nil {
return false
}
packageName = strings.ReplaceAll(packageName, " ", "")
if len(packageName) == 0 {
return false
}
return builtin.Contains(packages, packageName)
}
func (d *Device) ScreenCap() ([]byte, error) {
if d.HasFeature(FeatShellV2) {
return d.RunShellCommandV2WithBytes("screencap", "-p")

View File

@@ -60,7 +60,10 @@ func TestDevice_Product(t *testing.T) {
for i := range devices {
dev := devices[i]
product := dev.Product()
product, err := dev.Product()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), product)
}
}
@@ -70,7 +73,24 @@ func TestDevice_Model(t *testing.T) {
for i := range devices {
dev := devices[i]
t.Log(dev.Serial(), dev.Model())
model, err := dev.Model()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), model)
}
}
func TestDevice_Brand(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
brand, err := dev.Brand()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), brand)
}
}
@@ -79,7 +99,15 @@ func TestDevice_Usb(t *testing.T) {
for i := range devices {
dev := devices[i]
t.Log(dev.Serial(), dev.Usb(), dev.IsUsb())
usb, err := dev.Usb()
if err != nil {
t.Fatal(err)
}
isUsb, err := dev.IsUsb()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), usb, isUsb)
}
}
@@ -315,6 +343,22 @@ func TestDevice_InstallAPK(t *testing.T) {
}
}
func TestDevice_ListPackages(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
res, err := dev.ListPackages()
if err != nil {
t.Fatal(err)
}
t.Log(res)
installed, err := dev.IsPackagesInstalled("io.appium.uiautomator2.server")
if err != nil {
t.Fatal(err)
}
t.Log(installed)
}
}
func TestDevice_HasFeature(t *testing.T) {
setupDevices(t)

View File

@@ -320,6 +320,11 @@ func NewActionOptions(options ...ActionOption) *ActionOptions {
return actionOptions
}
type TapTextAction struct {
Text string
Options []ActionOption
}
type ActionOption func(o *ActionOptions)
func WithCustomOption(key string, value interface{}) ActionOption {

View File

@@ -2,27 +2,30 @@ package uixt
import (
"bytes"
"encoding/xml"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/funplugin/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
)
const AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
const UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
const (
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
)
type adbDriver struct {
Driver
@@ -31,6 +34,56 @@ type adbDriver struct {
logcat *AdbLogcat
}
type Hierarchy struct {
XMLName xml.Name `xml:"hierarchy"`
Nodes []Node `xml:"node"`
}
type Node struct {
Index string `xml:"index,attr"`
Text string `xml:"text,attr"`
ResourceID string `xml:"resource-id,attr"`
Class string `xml:"class,attr"`
Package string `xml:"package,attr"`
ContentDesc string `xml:"content-desc,attr"`
Checkable string `xml:"checkable,attr"`
Checked string `xml:"checked,attr"`
Clickable string `xml:"clickable,attr"`
Enabled string `xml:"enabled,attr"`
Focusable string `xml:"focusable,attr"`
Focused string `xml:"focused,attr"`
Scrollable string `xml:"scrollable,attr"`
LongClickable string `xml:"long-clickable,attr"`
Password string `xml:"password,attr"`
Selected string `xml:"selected,attr"`
Bounds *Bounds `xml:"bounds,attr"`
Children []Node `xml:"node"`
}
type Bounds struct {
X1, Y1, X2, Y2 int
}
func (b *Bounds) UnmarshalXMLAttr(attr xml.Attr) error {
// 正则表达式用于解析格式为"[x1,y1][x2,y2]"
re := regexp.MustCompile(`\[(\d+),(\d+)]\[(\d+),(\d+)]`)
matches := re.FindStringSubmatch(attr.Value)
if matches == nil {
return fmt.Errorf("bounds format is incorrect")
}
// 转换字符串为整数
b.X1, _ = strconv.Atoi(matches[1])
b.Y1, _ = strconv.Atoi(matches[2])
b.X2, _ = strconv.Atoi(matches[3])
b.Y2, _ = strconv.Atoi(matches[4])
return nil
}
// Center 方法计算并返回 Bounds 中心点的坐标
func (b *Bounds) Center() (float64, float64) {
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
}
func NewAdbDriver() *adbDriver {
log.Info().Msg("init adb driver")
return &adbDriver{}
@@ -479,10 +532,95 @@ func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
}
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
err = errDriverNotImplemented
_, err = ad.adbClient.RunShellCommand("uiautomator", "dump")
if err != nil {
return
}
source, err = ad.adbClient.RunShellCommand("cat", "/sdcard/window_dump.xml")
if err != nil {
return
}
return
}
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
source, err := ad.Source()
sourceTree = new(Hierarchy)
err = xml.Unmarshal([]byte(source), sourceTree)
if err != nil {
return
}
return
}
func (ad *adbDriver) TapByText(text string, options ...ActionOption) error {
sourceTree, err := ad.sourceTree()
if err != nil {
return err
}
return ad.tapByTextUsingHierarchy(sourceTree, text, options...)
}
func (ad *adbDriver) tapByTextUsingHierarchy(hierarchy *Hierarchy, text string, options ...ActionOption) error {
bounds := ad.searchNodes(hierarchy.Nodes, text, options...)
actionOptions := NewActionOptions(options...)
if len(bounds) == 0 {
if actionOptions.IgnoreNotFoundError {
log.Info().Msg("not found element by text " + text)
return nil
}
return errors.New("not found element by text " + text)
}
for _, bound := range bounds {
width, height := bound.Center()
err := ad.TapFloat(width, height, options...)
if err != nil {
return err
}
}
return nil
}
func (ad *adbDriver) TapByTexts(actions ...TapTextAction) error {
sourceTree, err := ad.sourceTree()
if err != nil {
return err
}
for _, action := range actions {
err := ad.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
if err != nil {
return err
}
}
return nil
}
func (ad *adbDriver) searchNodes(nodes []Node, text string, options ...ActionOption) []Bounds {
actionOptions := NewActionOptions(options...)
var results []Bounds
for _, node := range nodes {
result := ad.searchNodes(node.Children, text, options...)
results = append(results, result...)
if actionOptions.Regex {
// regex on, check if match regex
if !regexp.MustCompile(text).MatchString(node.Text) {
continue
}
} else {
// regex off, check if match exactly
if node.Text != text {
ad.searchNodes(node.Children, text, options...)
continue
}
}
if node.Bounds != nil {
results = append(results, *node.Bounds)
}
}
return results
}
func (ad *adbDriver) AccessibleSource() (source string, err error) {
err = errDriverNotImplemented
return
@@ -517,7 +655,7 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
}
// start logcat
err = ad.logcat.CatchLogcat()
err = ad.logcat.CatchLogcat("iesqaMonitor:V")
if err != nil {
err = errors.Wrap(code.AndroidCaptureLogError,
fmt.Sprintf("start adb log recording failed: %v", err))
@@ -527,17 +665,15 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
}
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
log.Info().Msg("stop adb log recording")
err = ad.logcat.Stop()
if err != nil {
log.Error().Err(err).Msg("failed to get adb log recording")
err = errors.Wrap(code.AndroidCaptureLogError,
fmt.Sprintf("get adb log recording failed: %v", err))
return "", err
}
content := ad.logcat.logBuffer.String()
log.Info().Str("logcat content", content).Msg("display logcat content")
pointRes := ConvertPoints(content)
defer func() {
log.Info().Msg("stop adb log recording")
err = ad.logcat.Stop()
if err != nil {
log.Error().Err(err).Msg("failed to get adb log recording")
}
}()
pointRes := ConvertPoints(ad.logcat.reader)
// 没有解析到打点日志,走兜底逻辑
if len(pointRes) == 0 {
log.Info().Msg("action log is null, use action file >>>")
@@ -551,7 +687,6 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
}
return nil
})
// 先保持原有状态码不变这里不return error
if err != nil {
log.Error().Err(err).Msg("read log file fail")
@@ -563,13 +698,13 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
return pointRes, nil
}
data, err := ioutil.ReadFile(files[0])
reader, err := os.Open(files[0])
if err != nil {
log.Info().Msg("read File error")
log.Info().Msg("open File error")
return pointRes, nil
}
pointRes = ConvertPoints(string(data))
pointRes = ConvertPoints(reader)
}
return pointRes, nil
}

View File

@@ -1,18 +1,19 @@
package uixt
import (
"bufio"
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net"
"os/exec"
"strings"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/funplugin/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
@@ -23,7 +24,6 @@ var (
AdbServerPort = gadb.AdbServerPort // 5037
UIA2ServerHost = "localhost"
UIA2ServerPort = 6790
DeviceTempPath = "/data/local/tmp"
)
const forwardToPrefix = "forward-to-"
@@ -148,18 +148,6 @@ func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
return deviceList, nil
}
func encodeUnicode(c int32) string {
var buffer bytes.Buffer
// Convert each rune (character) into two bytes
buffer.WriteByte(byte(c >> 8))
buffer.WriteByte(byte(c & 0xFF))
// Convert buffer bytes to base64 encoding
encoded := base64.StdEncoding.EncodeToString(buffer.Bytes())
// Replace "/" with "," and remove trailing "="
encoded = strings.ReplaceAll(encoded, "/", ",")
return strings.TrimRight(encoded, "=")
}
type AndroidDevice struct {
d *gadb.Device
logcat *AdbLogcat
@@ -290,26 +278,27 @@ func getFreePort() (int, error) {
}
type AdbLogcat struct {
serial string
logBuffer *bytes.Buffer
errs []error
stopping chan struct{}
done chan struct{}
cmd *exec.Cmd
serial string
// logBuffer *bytes.Buffer
errs []error
stopping chan struct{}
done chan struct{}
cmd *exec.Cmd
reader io.Reader
}
func NewAdbLogcat(serial string) *AdbLogcat {
return &AdbLogcat{
serial: serial,
logBuffer: new(bytes.Buffer),
stopping: make(chan struct{}),
done: make(chan struct{}),
serial: serial,
// logBuffer: new(bytes.Buffer),
stopping: make(chan struct{}),
done: make(chan struct{}),
}
}
// CatchLogcatContext starts logcat with timeout context
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
if err = l.CatchLogcat(); err != nil {
if err = l.CatchLogcat(""); err != nil {
return
}
go func() {
@@ -344,7 +333,7 @@ func (l *AdbLogcat) Errors() (err error) {
return
}
func (l *AdbLogcat) CatchLogcat() (err error) {
func (l *AdbLogcat) CatchLogcat(filter string) (err error) {
if l.cmd != nil {
log.Warn().Msg("logcat already start")
return nil
@@ -354,12 +343,19 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
return
}
args := []string{"-s", l.serial, "logcat", "--format", "time"}
if filter != "" {
args = append(args, "-s", filter)
}
// start logcat
l.cmd = myexec.Command("adb", "-s", l.serial,
"logcat", "--format", "time", "-s", "iesqaMonitor:V")
l.cmd.Stderr = l.logBuffer
l.cmd.Stdout = l.logBuffer
l.cmd = myexec.Command("adb", args...)
// l.cmd.Stderr = l.logBuffer
// l.cmd.Stdout = l.logBuffer
reader, err := l.cmd.StdoutPipe()
if err != nil {
return err
}
l.reader = reader
if err = l.cmd.Start(); err != nil {
return
}
@@ -370,17 +366,7 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
}
l.done <- struct{}{}
}()
return
}
func (l *AdbLogcat) BufferedLogcat() (err error) {
// -d: dump the current buffered logcat result and exits
cmd := myexec.Command("adb", "-s", l.serial, "logcat", "-d")
cmd.Stdout = l.logBuffer
cmd.Stderr = l.logBuffer
if err = cmd.Run(); err != nil {
return
}
return
}
@@ -394,9 +380,11 @@ type ExportPoint struct {
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
}
func ConvertPoints(data string) (eps []ExportPoint) {
lines := strings.Split(data, "\n")
for _, line := range lines {
func ConvertPoints(reader io.Reader) (eps []ExportPoint) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
log.Info().Str("logcat content", line)
if strings.Contains(line, "ext") {
idx := strings.Index(line, "{")
if idx == -1 {

View File

@@ -3,8 +3,6 @@
package uixt
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
@@ -427,15 +425,15 @@ func TestDriver_AppTerminate(t *testing.T) {
}
}
func TestConvertPoints(t *testing.T) {
data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}"
eps := ConvertPoints(data)
if len(eps) != 3 {
t.Fatal()
}
jsons, _ := json.Marshal(eps)
println(fmt.Sprintf("%v", string(jsons)))
}
//func TestConvertPoints(t *testing.T) {
// data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}"
// eps := ConvertPoints(data)
// if len(eps) != 3 {
// t.Fatal()
// }
// jsons, _ := json.Marshal(eps)
// println(fmt.Sprintf("%v", string(jsons)))
//}
func TestDriver_ShellInputUnicode(t *testing.T) {
device, _ := NewAndroidDevice()
@@ -456,3 +454,16 @@ func TestDriver_ShellInputUnicode(t *testing.T) {
t.Log(os.WriteFile("s1.png", raw.Bytes(), 0o600))
}
func TestTapTexts(t *testing.T) {
setupAndroid(t)
actions := []TapTextAction{
{Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
{Text: "已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
{Text: "^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", Options: []ActionOption{WithRegex(true), WithIgnoreNotFoundError(true)}},
}
err := driverExt.Driver.TapByTexts(actions...)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -598,3 +598,11 @@ func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
source = reply.Value
return
}
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
return ud.adbDriver.TapByText(text, options...)
}
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
return ud.adbDriver.TapByTexts(actions...)
}

View File

@@ -589,6 +589,11 @@ type WebDriver interface {
// Source Return application elements tree
Source(srcOpt ...SourceOption) (string, error)
TapByText(text string, options ...ActionOption) error
TapByTexts(actions ...TapTextAction) error
// AccessibleSource Return application elements accessibility tree
AccessibleSource() (string, error)

View File

@@ -759,6 +759,14 @@ func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
return
}
func (wd *wdaDriver) TapByText(text string, options ...ActionOption) error {
return errDriverNotImplemented
}
func (wd *wdaDriver) TapByTexts(actions ...TapTextAction) error {
return errDriverNotImplemented
}
func (wd *wdaDriver) AccessibleSource() (source string, err error) {
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession