commands/FBAccessibilityCommands.py (199 lines of code) (raw):

#!/usr/bin/python # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. import os import re import fbchisellldbbase as fb import fbchisellldbobjecthelpers as objHelpers # This is the key corresponding to accessibility label in # _accessibilityElementsInContainer: ACCESSIBILITY_LABEL_KEY = 2001 def lldbcommands(): return [ FBPrintAccessibilityLabels(), FBPrintAccessibilityIdentifiers(), FBFindViewByAccessibilityLabelCommand(), ] class FBPrintAccessibilityLabels(fb.FBCommand): def name(self): return "pa11y" def description(self): return "Print accessibility labels of all views in hierarchy of <aView>" def args(self): return [ fb.FBCommandArgument( arg="aView", type="UIView*", help="The view to print the hierarchy of.", default="(id)[[UIApplication sharedApplication] keyWindow]", ) ] def run(self, arguments, options): forceStartAccessibilityServer() printAccessibilityHierarchy(arguments[0]) class FBPrintAccessibilityIdentifiers(fb.FBCommand): def name(self): return "pa11yi" def description(self): return "Print accessibility identifiers of all views in hierarchy of <aView>" def args(self): return [ fb.FBCommandArgument( arg="aView", type="UIView*", help="The view to print the hierarchy of.", default="(id)[[UIApplication sharedApplication] keyWindow]", ) ] def run(self, arguments, option): forceStartAccessibilityServer() printAccessibilityIdentifiersHierarchy(arguments[0]) class FBFindViewByAccessibilityLabelCommand(fb.FBCommand): def name(self): return "fa11y" def description(self): return ( "Find the views whose accessibility labels match labelRegex " "and puts the address of the first result on the clipboard." ) def args(self): return [ fb.FBCommandArgument( arg="labelRegex", type="string", help="The accessibility label regex to search the view hierarchy for.", ) ] def accessibilityGrepHierarchy(self, view, needle): a11yLabel = accessibilityLabel(view) # if we don't have any accessibility string - we should have some children if int(a11yLabel.GetValue(), 16) == 0: # We call private method that gives back all visible accessibility children # for view iOS 10 and higher if fb.evaluateBooleanExpression( "[UIView respondsToSelector:@selector(_accessibilityElementsAndContainersDescendingFromViews:options:sorted:)]" ): accessibilityElements = fb.evaluateObjectExpression( "[UIView _accessibilityElementsAndContainersDescendingFromViews:@[(id)%s] options:0 sorted:NO]" % view ) else: accessibilityElements = fb.evaluateObjectExpression( "[[[UIApplication sharedApplication] keyWindow] _accessibilityElementsInContainer:0 topLevel:%s includeKB:0]" % view ) accessibilityElementsCount = fb.evaluateIntegerExpression( "[%s count]" % accessibilityElements ) for index in range(0, accessibilityElementsCount): subview = fb.evaluateObjectExpression( "[%s objectAtIndex:%i]" % (accessibilityElements, index) ) self.accessibilityGrepHierarchy(subview, needle) elif re.match( r".*" + needle + ".*", a11yLabel.GetObjectDescription(), re.IGNORECASE ): classDesc = objHelpers.className(view) print( "({} {}) {}".format(classDesc, view, a11yLabel.GetObjectDescription()) ) # First element that is found is copied to clipboard if not self.foundElement: self.foundElement = True cmd = 'echo %s | tr -d "\n" | pbcopy' % view os.system(cmd) def run(self, arguments, options): forceStartAccessibilityServer() rootView = fb.evaluateObjectExpression( "[[UIApplication sharedApplication] keyWindow]" ) self.foundElement = False self.accessibilityGrepHierarchy(rootView, arguments[0]) def isRunningInSimulator(): return ( fb.evaluateExpressionValue("(id)[[UIDevice currentDevice] model]") .GetObjectDescription() .lower() .find("simulator") >= 0 ) or ( fb.evaluateExpressionValue("(id)[[UIDevice currentDevice] name]") .GetObjectDescription() .lower() .find("simulator") >= 0 ) def forceStartAccessibilityServer(): # We try to start accessibility server only if we don't have needed method active if not fb.evaluateBooleanExpression( "[UIView instancesRespondToSelector:@selector(_accessibilityElementsInContainer:)]" ): # Starting accessibility server is different for simulator and device if isRunningInSimulator(): fb.evaluateEffect( "[[UIApplication sharedApplication] accessibilityActivate]" ) else: fb.evaluateEffect( "[[[UIApplication sharedApplication] _accessibilityBundlePrincipalClass] _accessibilityStartServer]" ) def accessibilityLabel(view): # using Apple private API to get real value of accessibility string for element. return fb.evaluateExpressionValue( "(id)[%s accessibilityAttributeValue:%i]" % (view, ACCESSIBILITY_LABEL_KEY), False, ) def accessibilityIdentifier(view): return fb.evaluateExpressionValue( "(id)[{} accessibilityIdentifier]".format(view), False ) def accessibilityElements(view): if fb.evaluateBooleanExpression( "[UIView instancesRespondToSelector:@selector(accessibilityElements)]" ): a11yElements = fb.evaluateExpression( "(id)[%s accessibilityElements]" % view, False ) if int(a11yElements, 16) != 0: return a11yElements if fb.evaluateBooleanExpression( "[%s respondsToSelector:@selector(_accessibleSubviews)]" % view ): return fb.evaluateExpression("(id)[%s _accessibleSubviews]" % (view), False) else: return fb.evaluateObjectExpression( "[[[UIApplication sharedApplication] keyWindow] _accessibilityElementsInContainer:0 topLevel:%s includeKB:0]" % view ) def printAccessibilityHierarchy(view, indent=0): a11yLabel = accessibilityLabel(view) classDesc = objHelpers.className(view) indentString = " | " * indent # if we don't have any accessibility string - we should have some children if int(a11yLabel.GetValue(), 16) == 0: print(indentString + ("{} {}".format(classDesc, view))) # We call private method that gives back all visible accessibility children # for view a11yElements = accessibilityElements(view) accessibilityElementsCount = int( fb.evaluateExpression("(int)[%s count]" % a11yElements) ) for index in range(0, accessibilityElementsCount): subview = fb.evaluateObjectExpression( "[%s objectAtIndex:%i]" % (a11yElements, index) ) printAccessibilityHierarchy(subview, indent + 1) else: print( indentString + ("({} {}) {}".format(classDesc, view, a11yLabel.GetObjectDescription())) ) def printAccessibilityIdentifiersHierarchy(view, indent=0): a11yIdentifier = accessibilityIdentifier(view) classDesc = objHelpers.className(view) indentString = " | " * indent # if we don't have any accessibility identifier - we should have some children if int(a11yIdentifier.GetValue(), 16) == 0: print(indentString + ("{} {}".format(classDesc, view))) # We call private method that gives back all visible accessibility children # for view a11yElements = accessibilityElements(view) accessibilityElementsCount = int( fb.evaluateExpression("(int)[%s count]" % a11yElements) ) for index in range(0, accessibilityElementsCount): subview = fb.evaluateObjectExpression( "[%s objectAtIndex:%i]" % (a11yElements, index) ) printAccessibilityIdentifiersHierarchy(subview, indent + 1) else: print( indentString + ( "({} {}) {}".format( classDesc, view, a11yIdentifier.GetObjectDescription() ) ) )