美文网首页
WebDriverAgent(WDA)的配置使用及源码分析

WebDriverAgent(WDA)的配置使用及源码分析

作者: Peter杰 | 来源:发表于2020-11-11 20:26 被阅读0次

    随着移动互联网时代的发展,移动终端的自动化测试日益活跃,总体来看在Android平台上的自动化工具和实践比较多,IOS的UI自动化测试由于系统封闭的原因,一直不太成熟。本着不侵入工程和拥抱原生的原则实现一套自动化测试方案。自动化测试节省时间节省真机的成本,而且更高效的覆盖所有的iOS机型测试,避免每次上线前重复的人工回归测试,保证每次上线的版本稳定运行。

    在Xcode 8之前,基于UI Automation的自动化测试方案是比较好用且非常流行的。但在Xcode 8之后,苹果在instruments工具集中直接废除了Automation组件,转而支持使用UI Testing。

    UI Testing

    从Xcode 7开始,苹果提供了UI Testing框架,也就是我们在APP test工程中使用的XCTest的那一套东西。UI Testing包含几个重要的类,分别是XCUIApplication、XCUIElement、XCUIElementQuery。
    XCUIApplication
    代表正在测试的应用程序的实例,可以对APP进行启动、终止、传入参数等操作。

    • XCUIApplication

      • 代表正在测试的应用程序的实例,可以对APP进行启动、终止、传入参数等操作。
      - (void)launch;
      - (void)activate;
      - (void)terminate;
      @property (nonatomic, copy) NSArray <NSString *> *launchArguments;
      @property (nonatomic, copy) NSDictionary <NSString *, NSString *> *launchEnvironment;
      
      
      • XCUIApplication在iOS上提供了两个初始化接口
      //Returns a proxy for the application specified by the "Target Application" target setting.
      - (instancetype)init NS_DESIGNATED_INITIALIZER;
      
      //Returns a proxy for an application associated with the specified bundle identifier.
      - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier NS_DESIGNATED_INITIALIZER;
      
      

      其中initWithBundleIdentifier接口允许传入一个bundle id来操作指定APP。这个技术点是iOS APP能够自动化测试的关键所在。

    • XCUIElement
      表示界面上显示的UI元素。

    • XCUIElementQuery
      用于定位UI元素的查询对象。
      上述几个模块就是一个UI测试框架的核心能力,后面在写Appium的自动化脚本时也是一样的套路:启动APP->定位UI元素->触发操作。

    WebDriverAgent

    image.png

    WebDriverAgent是用于iOS的WebDriver服务器实现,可用于远程控制iOS设备。它允许您启动和终止应用程序,点击并滚动视图或确认屏幕上是否存在视图。这使其成为用于应用程序端到端测试或通用设备自动化的理想工具。它通过链接XCTest.framework和调用Apple的API来直接在设备上执行命令来工作。WebDriverAgent是Facebook开发和用于端到端测试的,并已被Appium成功采用。

    在2019年5月,Facebook开源了IDB,即“ iOS Development Bridge”,这是一个用于使iOS模拟器和设备自动化的命令行界面。我们目前正在将自己的内部项目从WDA迁移到IDB,并建议将其检查出来作为替代方案。

    有关IDB的更多信息:

    虽然git上不再得到Facebook的积极的维护,移动端主流测试框架依然要借助WDA来实现与iOS交互测试,你可以在appium中下载可运行WebDriverAgent

    准备工作

    安装 homebrew

    homebrew 是 Mac OS 下最优秀的包管理工具,没有之一。

    xcode-select --install
    ruby -e "$(curl -fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install)"
    
    
    安装 python

    脚本语言 python 用来编写模拟的用户操作。

    brew install python3
    
    
    安装 libimobiledevice

    libimobiledevice 是一个使用原生协议与苹果iOS设备进行通信的库。通过这个库我们的 Mac OS 能够轻松获得 iOS 设备的信息。
    brew install --HEAD libimobiledevice

    使用方法:
    • 查看 iOS 设备日志
      idevicesyslog

    • 查看链接设备的UDID
      idevice_id --list

    • 查看设备信息
      ideviceinfo

    • 获取设备时间
      idevicedate

    • 获取设备名称
      idevicename

    • 端口转发
      iproxy XXXX YYYY

    • 屏幕截图
      idevicescreenshot

    安装 Carthage

    Carthage 是一款iOS项目依赖管理工具,与 Cocoapods 有着相似的功能,可以帮助你方便的管理三方依赖。它会把三方依赖编译成 framework,以 framework 的形式将三方依赖加入到项目中进行使用和管理。
    WebDriverAgent 本身使用了 Carthage 管理项目依赖,因此需要提前安装 Carthage。

    brew install carthage
    
    

    源码分析

    1.WebDriverAgent如何建立连接的?

    webdriver协议是一套基于HTTP协议的JSON格式规范,协议规定了不同操作对应的格式。之所以需要这层协议,是因为iOS、Android、浏览器等都有自己的UI交互方式,通过这层”驱动层“屏蔽各平台的差异,就可以通过相同的方式进行自动化的UI操作,做网络爬虫常用的selenium是浏览器上实现webdriver的驱动,而WebDriverAgent则是iOS上实现webdriver的驱动。
    使用Xcode打开WebDriverAgent项目,连接上iPhone设备之后,选中WebDriverAgentRunner->Product->Test,则会在iPhone上安装一个名为WebDriverAgentRunner的APP,这个APP实际上是一个后台应用,直接点击ICON打开的话会退出。
    具体到代码层面,WebDriverAgentRunner的入口在UITestingUITests.m文件

    - (void)testRunner
    {
      FBWebServer *webServer = [[FBWebServer alloc] init];
      webServer.delegate = self;
      [webServer startServing];
    }
    
    
    - (void)startServing
    {
      [FBLogger logFmt:@"Built at %s %s", __DATE__, __TIME__];
      self.exceptionHandler = [FBExceptionHandler new];
      [self startHTTPServer];         //  初始化Server 并注册路由
      [self initScreenshotsBroadcaster];    //
    
      self.keepAlive = YES;
      NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
        //这里是WDA为了防止程序退出,写了一个死循环,自己手动维护主线程,监听或实现UI操作
      while (self.keepAlive &&
             [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
    }
    
    - (void)startHTTPServer
    {
      //初始化Server
      self.server = [[RoutingHTTPServer alloc] init];
      [self.server setRouteQueue:dispatch_get_main_queue()];
      [self.server setDefaultHeader:@"Server" value:@"WebDriverAgent/1.0"];
      [self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"];
      [self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"];
      [self.server setConnectionClass:[FBHTTPConnection self]];
    
      //注册所有路由
      [self registerRouteHandlers:[self.class collectCommandHandlerClasses]];
      [self registerServerKeyRouteHandlers];
    
      NSRange serverPortRange = FBConfiguration.bindingPortRange;
      NSError *error;
      BOOL serverStarted = NO;
    
      for (NSUInteger index = 0; index < serverPortRange.length; index++) {
        NSInteger port = serverPortRange.location + index;
        [self.server setPort:(UInt16)port];
    
        serverStarted = [self attemptToStartServer:self.server onPort:port withError:&error];
        if (serverStarted) {
          break;
        }
    
        [FBLogger logFmt:@"Failed to start web server on port %ld with error %@", (long)port, [error description]];
      }
    
      if (!serverStarted) {
        [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]];
        abort();
      }
      [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, [XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"localhost", [self.server port], FBServerURLEndMarker];
    }
    
    
    • WebDriverAgentRunner会在手机上8100端口启动一个HTTP server,startServing方法内部就是一个死循环,监听网络传输过来的webdriver协议的数据,解析并处理路由事件。
    • 在startHTTPServer里创建server并建立连接,调用registerRouteHandlers方法注册所有路由

    路由注册

    下面来看下注册路由 [self registerRouteHandlers:[self.class collectCommandHandlerClasses]]方法的源码
    首先来看[self.class collectCommandHandlerClasses]方法的实现

    //获取所有遵循FBCommandHandler协议的类
    + (NSArray<Class<FBCommandHandler>> *)collectCommandHandlerClasses
    {
        //利用runtime 动态获取所有注册过FBCommandHandler协议的类
      NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler));
      NSMutableArray *handlers = [NSMutableArray array];
    
        //筛选shouldRegisterAutomatically返回YES的类
      for (Class aClass in handlersClasses) {
        /*
        shouldRegisterAutomatically
        BOOL deciding if class should be added to route handlers automatically, default (if not implemented) is YES
        BOOL决定是否应将类自动添加到路由处理程序,默认(如果未实现)是
        */
        if ([aClass respondsToSelector:@selector(shouldRegisterAutomatically)]) {
          if (![aClass shouldRegisterAutomatically]) {
            continue;
          }
        }
        [handlers addObject:aClass];
      }
      return handlers.copy;
    }
    
    #import "FBRuntimeUtils.h"
    #import <objc/runtime.h>
    
    //利用runtime 动态获取注册过FBCommandHandler协议的
    NSArray<Class> *FBClassesThatConformsToProtocol(Protocol *protocol)
    {
      Class *classes = NULL;
      NSMutableArray *collection = [NSMutableArray array];
        /*获取到当前注册的所有类的总个数,它需要传入两个参数,
        第一个参数 buffer :已分配好内存空间的数组,传NULL会自动计算内存空间
        第二个参数 bufferCount :数组中可存放元素的个数,
        返回值是注册的类的总数。*/
      int numClasses = objc_getClassList(NULL, 0);
        //如果没有注册类,直接返回空数组
      if (numClasses == 0 ) {
        return @[];
      }
        //遍历所有注册的类,如果遵循FBCommandHandler协议,就添加到数组里
      classes = (__unsafe_unretained Class*)malloc(sizeof(Class) * numClasses);
      numClasses = objc_getClassList(classes, numClasses);
      for (int index = 0; index < numClasses; index++) {
        Class aClass = classes[index];
        if (class_conformsToProtocol(aClass, protocol)) {
          [collection addObject:aClass];
        }
      }
      free(classes);
      return collection.copy;
    }
    
    

    collectCommandHandlerClasses方法其实是利用runtime动态获取到所有注册过FBCommandHandler协议的类
    下面来看下registerRouteHandlers方法的实现

    - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses
    {
        //  遍历所有遵循FBCommandHandler协议的类
      for (Class<FBCommandHandler> commandHandler in commandHandlerClasses) {
        //  获取类实现的routes方法返回的路由数组
        NSArray *routes = [commandHandler routes];
    
        for (FBRoute *route in routes) {
          [self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) {
            //#warning 接收事件的回调
            NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL];
            FBRouteRequest *routeParams = [FBRouteRequest
              routeRequestWithURL:request.url
              parameters:request.params
              arguments:arguments ?: @{}
            ];
    
            [FBLogger verboseLog:routeParams.description];
    
            @try {
              [route mountRequest:routeParams intoResponse:response];
            }
            @catch (NSException *exception) {
              [self handleException:exception forResponse:response];
            }
          }];
        }
      }
    }
    
    - (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block {
        //创建路由,并解析path
      Route *route = [self routeWithPath:path];
        //每一个路由都持有一个对用的block,
      route.handler = block;
    
      [self addRoute:route forMethod:method];
    }
    
    //创建路由,并解析path
    - (Route *)routeWithPath:(NSString *)path {
      Route *route = [[Route alloc] init];//创建路由
      NSMutableArray *keys = [NSMutableArray array];
    
      if ([path length] > 2 && [path characterAtIndex:0] == '{') {
        // This is a custom regular expression, just remove the {}
        path = [path substringWithRange:NSMakeRange(1, [path length] - 2)];
      } else {
        NSRegularExpression *regex = nil;
    
        // Escape regex characters
        regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil];
        path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"];
    
        // Parse any :parameters and * in the path
        regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)"
                                                          options:0
                                                            error:nil];
        NSMutableString *regexPath = [NSMutableString stringWithString:path];
        __block NSInteger diff = 0;
        [regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length)
                             usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
          NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length);
          NSString *replacementString;
    
          NSString *capturedString = [path substringWithRange:result.range];
          if ([capturedString isEqualToString:@"*"]) {
            [keys addObject:@"wildcards"];
            replacementString = @"(.*?)";
          } else {
            NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]];
            [keys addObject:keyString];
            replacementString = @"([^/]+)";
          }
    
          [regexPath replaceCharactersInRange:replacementRange withString:replacementString];
          diff += replacementString.length - result.range.length;
        }];
    
        path = [NSString stringWithFormat:@"^%@$", regexPath];
      }
    
      route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil];
        //让route持有path
      if ([keys count] > 0) {
        route.keys = keys;
      }
    
      return route;
    }
    //添加路由到对应的方法
    - (void)addRoute:(Route *)route forMethod:(NSString *)method {
      //方法method排序
      method = [method uppercaseString];
      //以方法method为key,获取routes里的对应的数组,如果没有就创建一个数组作为value,存入routes
      NSMutableArray *methodRoutes = [routes objectForKey:method];
      if (methodRoutes == nil) {
        methodRoutes = [NSMutableArray array];
        [routes setObject:methodRoutes forKey:method];
      }
      //将route对象缓存在routes中
      [methodRoutes addObject:route];
    
      // Define a HEAD route for all GET routes
      if ([method isEqualToString:@"GET"]) {
        [self addRoute:route forMethod:@"HEAD"];
      }
    }
    
    

    以上是WDA注册路由的源码,原理是通过一个全局的字典routes,以方法method为key,存储对应的route路由对象,每一个route对象都会有一个path和block,当接收到对应的path指令时去执行block。那么path指令是在何时接收的呢?

    建立连接接受指令

    在RoutingHTTPServer中,搜索routes objectForKey:,我们发现了这个方法

    - (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)httpMessage connection:(HTTPConnection *)connection {
        //routes中找出路由对象
      NSMutableArray *methodRoutes = [routes objectForKey:method];
      if (methodRoutes == nil)
        return nil;
    
      for (Route *route in methodRoutes) {
        NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)];
        if (!result)
          continue;
    
        // The first range is all of the text matched by the regex.
        NSUInteger captureCount = [result numberOfRanges];
    
        if (route.keys) {
          // Add the route's parameters to the parameter dictionary, accounting for
          // the first range containing the matched text.
          if (captureCount == [route.keys count] + 1) {
            NSMutableDictionary *newParams = [params mutableCopy];
            NSUInteger index = 1;
            BOOL firstWildcard = YES;
            for (NSString *key in route.keys) {
              NSString *capture = [path substringWithRange:[result rangeAtIndex:index]];
              if ([key isEqualToString:@"wildcards"]) {
                NSMutableArray *wildcards = [newParams objectForKey:key];
                if (firstWildcard) {
                  // Create a new array and replace any existing object with the same key
                  wildcards = [NSMutableArray array];
                  [newParams setObject:wildcards forKey:key];
                  firstWildcard = NO;
                }
                [wildcards addObject:capture];
              } else {
                [newParams setObject:capture forKey:key];
              }
              index++;
            }
            params = newParams;
          }
        } else if (captureCount > 1) {
          // For custom regular expressions place the anonymous captures in the captures parameter
          NSMutableDictionary *newParams = [params mutableCopy];
          NSMutableArray *captures = [NSMutableArray array];
          for (NSUInteger i = 1; i < captureCount; i++) {
            [captures addObject:[path substringWithRange:[result rangeAtIndex:i]]];
          }
          [newParams setObject:captures forKey:@"captures"];
          params = newParams;
        }
    
        RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params];
        RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection];
        if (!routeQueue) {
          [self handleRoute:route withRequest:request response:response];
        } else {
          // Process the route on the specified queue
          dispatch_sync(routeQueue, ^{
            @autoreleasepool {
              [self handleRoute:route withRequest:request response:response];
            }
          });
        }
        return response;
      }
    
      return nil;
    }
    
    

    顺藤摸瓜,查找的这个方法的调用,在HTTPConnection类中replyToHTTPRequest方法里

    - (void)replyToHTTPRequest
    {
      HTTPLogTrace();
    
      if (HTTP_LOG_VERBOSE)
      {
        NSData *tempData = [request messageData];
    
        NSString *tempStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
        HTTPLogVerbose(@"%@[%p]: Received HTTP request:\n%@", THIS_FILE, self, tempStr);
      }
    
      // Check the HTTP version
      // We only support version 1.0 and 1.1
    
      NSString *version = [request version];
      if (![version isEqualToString:HTTPVersion1_1] && ![version isEqualToString:HTTPVersion1_0])
      {
        [self handleVersionNotSupported:version];
        return;
      }
    
      // Extract requested URI
      NSString *uri = [self requestURI];
    
      // Extract the method
      NSString *method = [request method];
    
      // Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag:
    
      // Respond properly to HTTP 'GET' and 'HEAD' commands
        //这里调用的解析方法
      httpResponse = [self httpResponseForMethod:method URI:uri];
    
      if (httpResponse == nil)
      {
        [self handleResourceNotFound];
        return;
      }
    
      [self sendResponseHeadersAndBody];
    }
    
    

    最终我们在GCDAsyncSocket Delegate 里找到了该方法的调用

    - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)tag
    
    

    GCDAsyncSocket是Server长链接,只要有消息过来就会调用GCDAsyncSocket 的Delegate方法

    2.WebDriverAgent如何查找元素的?

    刚刚有讲过WDA中路由的注册,从WebDriverAgent的源码可以清晰的看到,在Commands目录,是支持的操作类集合。每一个操作都通过routes类方法注册对应的路由和处理该路由的函数。查找元素路由注册放在FBFindElementCommands.m

    @implementation FBFindElementCommands
    
    #pragma mark - <FBCommandHandler>
    
    + (NSArray *)routes
    {
      return
      @[
        [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)],
        [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)],
        [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)],
        [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)],
        [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)],
    #if TARGET_OS_TV
        [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetFocusedElement:)],
    #else
        [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)],
    #endif
      ];
    }
    
    

    以handleFindElement为例,代码追踪

    + (id<FBResponsePayload>)handleFindElement:(FBRouteRequest *)request
    {
      FBSession *session = request.session;
        /*
       Using:查找的方式
       Value:依据Value去查找元素
       under:从under开始查找元素 session.activeApplication继承自XCUIApplication
       */
      XCUIElement *element = [self.class elementUsing:request.arguments[@"using"]
                                            withValue:request.arguments[@"value"]
                                                under:session.activeApplication];
      if (!element) {
        return FBNoSuchElementErrorResponseForRequest(request);
      }
      return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
    }
    
    + (XCUIElement *)elementUsing:(NSString *)usingText withValue:(NSString *)value under:(XCUIElement *)element
    {
      return [[self elementsUsing:usingText
                        withValue:value
                            under:element
      shouldReturnAfterFirstMatch:YES] firstObject];
    }
    
    + (NSArray *)elementsUsing:(NSString *)usingText withValue:(NSString *)value under:(XCUIElement *)element shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      NSArray *elements;
      const BOOL partialSearch = [usingText isEqualToString:@"partial link text"];
      const BOOL isSearchByIdentifier = ([usingText isEqualToString:@"name"] || [usingText isEqualToString:@"id"] || [usingText isEqualToString:@"accessibility id"]);
      if (partialSearch || [usingText isEqualToString:@"link text"]) {
        NSArray *components = [value componentsSeparatedByString:@"="];
        NSString *propertyValue = components.lastObject;
        NSString *propertyName = (components.count < 2 ? @"name" : components.firstObject);
        elements = [element fb_descendantsMatchingProperty:propertyName value:propertyValue partialSearch:partialSearch];
      } else if ([usingText isEqualToString:@"class name"]) {
        elements = [element fb_descendantsMatchingClassName:value shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
      } else if ([usingText isEqualToString:@"class chain"]) {
        elements = [element fb_descendantsMatchingClassChain:value shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
      } else if ([usingText isEqualToString:@"xpath"]) {
        elements = [element fb_descendantsMatchingXPathQuery:value shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
      } else if ([usingText isEqualToString:@"predicate string"]) {
        NSPredicate *predicate = [FBPredicate predicateWithFormat:value];
        elements = [element fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
      } else if (isSearchByIdentifier) {
        elements = [element fb_descendantsMatchingIdentifier:value shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
      } else {
        [[NSException exceptionWithName:FBElementAttributeUnknownException reason:[NSString stringWithFormat:@"Invalid locator requested: %@", usingText] userInfo:nil] raise];
      }
      return elements;
    }
    
    

    以上查找的方法放在了XCUIElement +FBFind 分类里

    /**
     * Copyright (c) 2015-present, Facebook, Inc.
     * All rights reserved.
     *
     * This source code is licensed under the BSD-style license found in the
     * LICENSE file in the root directory of this source tree. An additional grant
     * of patent rights can be found in the PATENTS file in the same directory.
     */
    
    #import "XCUIElement+FBFind.h"
    
    #import "FBMacros.h"
    #import "FBElementTypeTransformer.h"
    #import "FBPredicate.h"
    #import "NSPredicate+FBFormat.h"
    #import "XCElementSnapshot.h"
    #import "XCElementSnapshot+FBHelpers.h"
    #import "FBXCodeCompatibility.h"
    #import "XCUIElement+FBUtilities.h"
    #import "XCUIElement+FBWebDriverAttributes.h"
    #import "XCUIElementQuery.h"
    #import "FBElementUtils.h"
    #import "FBXCodeCompatibility.h"
    #import "FBXPath.h"
    
    @implementation XCUIElement (FBFind)
    
    + (NSArray<XCUIElement *> *)fb_extractMatchingElementsFromQuery:(XCUIElementQuery *)query shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      if (!shouldReturnAfterFirstMatch) {
        return query.fb_allMatches;
      }
      XCUIElement *matchedElement = query.fb_firstMatch;
      return matchedElement ? @[matchedElement] : @[];
    }
    
    #pragma mark - Search by ClassName
    
    - (NSArray<XCUIElement *> *)fb_descendantsMatchingClassName:(NSString *)className shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      NSMutableArray *result = [NSMutableArray array];
      XCUIElementType type = [FBElementTypeTransformer elementTypeWithTypeName:className];
      if (self.elementType == type || type == XCUIElementTypeAny) {
        [result addObject:self];
        if (shouldReturnAfterFirstMatch) {
          return result.copy;
        }
      }
      XCUIElementQuery *query = [self.fb_query descendantsMatchingType:type];
      [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
      return result.copy;
    }
    
    #pragma mark - Search by property value
    
    - (NSArray<XCUIElement *> *)fb_descendantsMatchingProperty:(NSString *)property value:(NSString *)value partialSearch:(BOOL)partialSearch
    {
      NSMutableArray *elements = [NSMutableArray array];
      [self descendantsWithProperty:property value:value partial:partialSearch results:elements];
      return elements;
    }
    
    - (void)descendantsWithProperty:(NSString *)property value:(NSString *)value partial:(BOOL)partialSearch results:(NSMutableArray<XCUIElement *> *)results
    {
      if (partialSearch) {
        NSString *text = [self fb_valueForWDAttributeName:property];
        BOOL isString = [text isKindOfClass:[NSString class]];
        if (isString && [text rangeOfString:value].location != NSNotFound) {
          [results addObject:self];
        }
      } else {
        if ([[self fb_valueForWDAttributeName:property] isEqual:value]) {
          [results addObject:self];
        }
      }
    
      property = [FBElementUtils wdAttributeNameForAttributeName:property];
      value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
      NSString *operation = partialSearch ?
      [NSString stringWithFormat:@"%@ like '*%@*'", property, value] :
      [NSString stringWithFormat:@"%@ == '%@'", property, value];
    
      NSPredicate *predicate = [FBPredicate predicateWithFormat:operation];
      XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:predicate];
      NSArray *childElements = query.fb_allMatches;
      [results addObjectsFromArray:childElements];
    }
    
    #pragma mark - Search by Predicate String
    
    - (NSArray<XCUIElement *> *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      NSPredicate *formattedPredicate = [NSPredicate fb_formatSearchPredicate:predicate];
      NSMutableArray<XCUIElement *> *result = [NSMutableArray array];
      // Include self element into predicate search
      if ([formattedPredicate evaluateWithObject:self.fb_cachedSnapshot ?: self.fb_lastSnapshot]) {
        if (shouldReturnAfterFirstMatch) {
          return @[self];
        }
        [result addObject:self];
      }
      XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:formattedPredicate];
      [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
      return result.copy;
    }
    
    #pragma mark - Search by xpath
    
    - (NSArray<XCUIElement *> *)fb_descendantsMatchingXPathQuery:(NSString *)xpathQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      // XPath will try to match elements only class name, so requesting elements by XCUIElementTypeAny will not work. We should use '*' instead.
      xpathQuery = [xpathQuery stringByReplacingOccurrencesOfString:@"XCUIElementTypeAny" withString:@"*"];
      NSArray<XCElementSnapshot *> *matchingSnapshots = [FBXPath matchesWithRootElement:self forQuery:xpathQuery];
      if (0 == [matchingSnapshots count]) {
        return @[];
      }
      if (shouldReturnAfterFirstMatch) {
        XCElementSnapshot *snapshot = matchingSnapshots.firstObject;
        matchingSnapshots = @[snapshot];
      }
      return [self fb_filterDescendantsWithSnapshots:matchingSnapshots selfUID:nil onlyChildren:NO];
    }
    
    #pragma mark - Search by Accessibility Id
    
    - (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      NSMutableArray *result = [NSMutableArray array];
      if (self.identifier == accessibilityId) {
        [result addObject:self];
        if (shouldReturnAfterFirstMatch) {
          return result.copy;
        }
      }
      XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:accessibilityId];
      [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
      return result.copy;
    }
    
    @end
    
    

    以ClassName为例

    - (NSArray<XCUIElement *> *)fb_descendantsMatchingClassName:(NSString *)className shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
    
      NSMutableArray *result = [NSMutableArray array];
      //根据类名获取元素类型
      XCUIElementType type = [FBElementTypeTransformer elementTypeWithTypeName:className];
      if (self.elementType == type || type == XCUIElementTypeAny) {
        [result addObject:self];
        if (shouldReturnAfterFirstMatch) {
          return result.copy;
        }
      }
      //获取当前元素的XCUIElementQuery
        //self.fb_query见下图
      XCUIElementQuery *query = [self.fb_query descendantsMatchingType:type];
      [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
      return result.copy;
    }
    
    + (NSArray<XCUIElement *> *)fb_extractMatchingElementsFromQuery:(XCUIElementQuery *)query shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
    {
      if (!shouldReturnAfterFirstMatch) {
        return query.fb_allMatches;
      }
      XCUIElement *matchedElement = query.fb_firstMatch;
      return matchedElement ? @[matchedElement] : @[];
    }
    
    - (XCUIElement *)fb_firstMatch
    {
      XCUIElement* match = FBConfiguration.useFirstMatch
        ? self.firstMatch
        : self.fb_allMatches.firstObject;
      return [match exists] ? match : nil;
    }
    - (NSArray<XCUIElement *> *)fb_allMatches
    {
      return FBConfiguration.boundElementsByIndex
        ? self.allElementsBoundByIndex
        : self.allElementsBoundByAccessibilityElement;
    }
    
    
    image.png

    最终会根据你的查找方式(usingText)去查找XCUIElement元素。

    image.png

    最终的查找会通过XCUIElement私有属性allElementsBoundByAccessibilityElement,allElementsBoundByIndex去拿到到需要的Element。
    allElementsBoundByAccessibilityElement
    query中根据accessibility element得到的元素数组。得到XCUIElement数组
    allElementsBoundByIndex
    query中根据索引值得到的元素数组。得到XCUIElement数组

    3.WebDriverAgent如何处理点击事件的?

    同样是在Commands目录下,Touch事件路由注册放在FBTouchActionCommands.m中,


    image.png

    可以看到/wda/touch/perform、/wda/touch/multi/perform、/actions路由负责处理不同的点击事件。那么当一个点击的url请求过来时,如何转化为iOS的UIEvent事件呢?跟踪代码

    image.png
    + (id<FBResponsePayload>)handlePerformAppiumTouchActions:(FBRouteRequest *)request
    {
      XCUIApplication *application = request.session.activeApplication;
      NSArray *actions = (NSArray *)request.arguments[@"actions"];
      NSError *error;
      if (![application fb_performAppiumTouchActions:actions elementCache:request.session.elementCache error:&error]) {
        return FBResponseWithUnknownError(error);
      }
      return FBResponseWithOK();
    }
    - (BOOL)fb_performAppiumTouchActions:(NSArray *)actions elementCache:(FBElementCache *)elementCache error:(NSError **)error
    {
      return [self fb_performActionsWithSynthesizerType:FBAppiumActionsSynthesizer.class actions:actions elementCache:elementCache error:error];
    }
    - (BOOL)fb_performActionsWithSynthesizerType:(Class)synthesizerType actions:(NSArray *)actions elementCache:(FBElementCache *)elementCache error:(NSError **)error
    {
    
      //将actions事件生成synthesizer对象
      FBBaseActionsSynthesizer *synthesizer = [[synthesizerType alloc] initWithActions:actions forApplication:self elementCache:elementCache error:error];
      if (nil == synthesizer) {
        return NO;
      }
      //synthesizer生成eventRecord
      XCSynthesizedEventRecord *eventRecord = [synthesizer synthesizeWithError:error];
      if (nil == eventRecord) {
        return [self.class handleEventSynthesWithError:*error];
      }
      return [self fb_synthesizeEvent:eventRecord error:error];
    }
    - (BOOL)fb_synthesizeEvent:(XCSynthesizedEventRecord *)event error:(NSError *__autoreleasing*)error
    {
      return [FBXCTestDaemonsProxy synthesizeEventWithRecord:event error:error];
    }
    
    + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
    {
      __block BOOL didSucceed = NO;
      [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
        void (^errorHandler)(NSError *) = ^(NSError *invokeError) {
          if (error) {
            *error = invokeError;
          }
          didSucceed = (invokeError == nil);
          completion();
        };
    
        if (nil == FBXCTRunnerDaemonSessionClass) {
          [[self testRunnerProxy] _XCT_synthesizeEvent:record completion:errorHandler];
        } else {
          XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
            errorHandler(invokeError);
          };
          if ([XCUIDevice.sharedDevice respondsToSelector:@selector(eventSynthesizer)]) {
            //核心代码
            [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
              handlerBlock(record, invokeError);
            }];
          } else {
            [[FBXCTRunnerDaemonSessionClass sharedSession] synthesizeEvent:record completion:^(NSError *invokeError){
              handlerBlock(record, invokeError);
            }];
          }
        }
      }];
      return didSucceed;
    }
    
    

    发现核心代码是:


    image.png

    XCUIDevice的eventSynthesizer是私有方法,通过synthesizeEvent发送XCSynthesizedEventRecord(也是私有类)事件。到这里WebDriverAgent的流程就很清楚了。实际上由于使用了很多私有方法,WebDriverAgent并非仅能自动化当前APP,也是可以操作手机屏幕以及任意APP的。

    总结

    1、WDA为了防止程序退出,写了一个死循环,利用RunLoop手动维护主线程,监听或实现UI操作
    2、RoutingHTTPServer继承自HTTPServer,HTTPServer内部对GCDAsyncSocket进行封装。HTTPConnection里实现了GCDAsyncSocket的代理方法。所以WDA内部是利用GCDAsyncSocket长连接,与appium进行通信。
    3、对于元素的查找,WDA是利用了XCUIElementQuery进行element查找
    利用XCUIApplication的launch方法来开启指定app。
    4、对于实现UI事件,XCUIDevice的eventSynthesizer是私有方法,通过synthesizeEvent发送XCSynthesizedEventRecord(也是私有类)事件。

    相关文章

      网友评论

          本文标题:WebDriverAgent(WDA)的配置使用及源码分析

          本文链接:https://www.haomeiwen.com/subject/pnxrbktx.html