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()
)
)
)