appium-webdriveragent 16.1.3 → 16.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +297 -0
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_tvOS.xcscheme +78 -0
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_tvOS.xcscheme +86 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +2 -2
- package/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m +1 -1
- package/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +4 -4
- package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +33 -0
- package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +23 -0
- package/WebDriverAgentLib/Commands/FBElementCommands.m +8 -3
- package/WebDriverAgentLib/FBAlert.m +10 -2
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +10 -0
- package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +29 -4
- package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +62 -27
- package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.h +13 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.m +26 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/Info.plist +41 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.h +15 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.m +22 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.h +13 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.m +79 -0
- package/WebDriverAgentTests/IntegrationApp_tvOS/main.m +17 -0
- package/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +84 -0
- package/WebDriverAgentTests/IntegrationTests_tvOS/Info.plist +24 -0
- package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.h +27 -0
- package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.m +23 -0
- package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.h +43 -0
- package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.m +24 -0
- package/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Scheme
|
|
3
|
+
LastUpgradeVersion = "2600"
|
|
4
|
+
version = "1.3">
|
|
5
|
+
<BuildAction
|
|
6
|
+
parallelizeBuildables = "YES"
|
|
7
|
+
buildImplicitDependencies = "YES">
|
|
8
|
+
<BuildActionEntries>
|
|
9
|
+
<BuildActionEntry
|
|
10
|
+
buildForTesting = "YES"
|
|
11
|
+
buildForRunning = "YES"
|
|
12
|
+
buildForProfiling = "NO"
|
|
13
|
+
buildForArchiving = "NO"
|
|
14
|
+
buildForAnalyzing = "NO">
|
|
15
|
+
<BuildableReference
|
|
16
|
+
BuildableIdentifier = "primary"
|
|
17
|
+
BlueprintIdentifier = "DE52ACA4B43F527189E374EE"
|
|
18
|
+
BuildableName = "IntegrationTests_tvOS.xctest"
|
|
19
|
+
BlueprintName = "IntegrationTests_tvOS"
|
|
20
|
+
ReferencedContainer = "container:WebDriverAgent.xcodeproj">
|
|
21
|
+
</BuildableReference>
|
|
22
|
+
</BuildActionEntry>
|
|
23
|
+
</BuildActionEntries>
|
|
24
|
+
</BuildAction>
|
|
25
|
+
<TestAction
|
|
26
|
+
buildConfiguration = "Debug"
|
|
27
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
28
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
29
|
+
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
30
|
+
<Testables>
|
|
31
|
+
<TestableReference
|
|
32
|
+
skipped = "NO">
|
|
33
|
+
<BuildableReference
|
|
34
|
+
BuildableIdentifier = "primary"
|
|
35
|
+
BlueprintIdentifier = "DE52ACA4B43F527189E374EE"
|
|
36
|
+
BuildableName = "IntegrationTests_tvOS.xctest"
|
|
37
|
+
BlueprintName = "IntegrationTests_tvOS"
|
|
38
|
+
ReferencedContainer = "container:WebDriverAgent.xcodeproj">
|
|
39
|
+
</BuildableReference>
|
|
40
|
+
</TestableReference>
|
|
41
|
+
</Testables>
|
|
42
|
+
</TestAction>
|
|
43
|
+
<LaunchAction
|
|
44
|
+
buildConfiguration = "Debug"
|
|
45
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
46
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
47
|
+
launchStyle = "0"
|
|
48
|
+
useCustomWorkingDirectory = "NO"
|
|
49
|
+
ignoresPersistentStateOnLaunch = "NO"
|
|
50
|
+
debugDocumentVersioning = "YES"
|
|
51
|
+
debugServiceExtension = "internal"
|
|
52
|
+
allowLocationSimulation = "YES">
|
|
53
|
+
<MacroExpansion>
|
|
54
|
+
<BuildableReference
|
|
55
|
+
BuildableIdentifier = "primary"
|
|
56
|
+
BlueprintIdentifier = "DE52ACA4B43F527189E374EE"
|
|
57
|
+
BuildableName = "IntegrationTests_tvOS.xctest"
|
|
58
|
+
BlueprintName = "IntegrationTests_tvOS"
|
|
59
|
+
ReferencedContainer = "container:WebDriverAgent.xcodeproj">
|
|
60
|
+
</BuildableReference>
|
|
61
|
+
</MacroExpansion>
|
|
62
|
+
</LaunchAction>
|
|
63
|
+
<ProfileAction
|
|
64
|
+
buildConfiguration = "Release"
|
|
65
|
+
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
66
|
+
savedToolIdentifier = ""
|
|
67
|
+
useCustomWorkingDirectory = "NO"
|
|
68
|
+
debugDocumentVersioning = "YES">
|
|
69
|
+
<MacroExpansion>
|
|
70
|
+
<BuildableReference
|
|
71
|
+
BuildableIdentifier = "primary"
|
|
72
|
+
BlueprintIdentifier = "DE52ACA4B43F527189E374EE"
|
|
73
|
+
BuildableName = "IntegrationTests_tvOS.xctest"
|
|
74
|
+
BlueprintName = "IntegrationTests_tvOS"
|
|
75
|
+
ReferencedContainer = "container:WebDriverAgent.xcodeproj">
|
|
76
|
+
</BuildableReference>
|
|
77
|
+
</MacroExpansion>
|
|
78
|
+
</ProfileAction>
|
|
79
|
+
<AnalyzeAction
|
|
80
|
+
buildConfiguration = "Debug">
|
|
81
|
+
</AnalyzeAction>
|
|
82
|
+
<ArchiveAction
|
|
83
|
+
buildConfiguration = "Release"
|
|
84
|
+
revealArchiveInOrganizer = "YES">
|
|
85
|
+
</ArchiveAction>
|
|
86
|
+
</Scheme>
|
|
@@ -135,7 +135,7 @@ static NSString *const FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID = @"com.apple.Contacts
|
|
|
135
135
|
|
|
136
136
|
// In case of iPad we want to check if sheet isn't contained by popover.
|
|
137
137
|
// In that case we ignore it.
|
|
138
|
-
id<FBXCElementSnapshot> sheetSnapshot = sheet.
|
|
138
|
+
id<FBXCElementSnapshot> sheetSnapshot = sheet.fb_cachedSnapshot ?: [sheet fb_customSnapshot];
|
|
139
139
|
BOOL isInsidePopover = NO;
|
|
140
140
|
id<FBXCElementSnapshot> ancestor = sheetSnapshot.parent;
|
|
141
141
|
while (nil != ancestor) {
|
|
@@ -154,7 +154,7 @@ static NSString *const FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID = @"com.apple.Contacts
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
for (XCUIElement *scrollView in scrollViews) {
|
|
157
|
-
id<FBXCElementSnapshot> scrollViewSnapshot = scrollView.
|
|
157
|
+
id<FBXCElementSnapshot> scrollViewSnapshot = scrollView.fb_cachedSnapshot ?: [scrollView fb_customSnapshot];
|
|
158
158
|
id<FBXCElementSnapshot> app = [[FBXCElementSnapshotWrapper ensureWrapped:scrollViewSnapshot] fb_parentMatchingType:XCUIElementTypeApplication];
|
|
159
159
|
if (nil == app || ![app.label isEqualToString:FB_SAFARI_APP_NAME]) {
|
|
160
160
|
continue;
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
// Reuse an already-taken snapshot of `self` if one is available (e.g. the
|
|
138
138
|
// caller just resolved/inspected this same element) instead of always
|
|
139
139
|
// paying for a fresh one.
|
|
140
|
-
NSArray<id<FBXCElementSnapshot>> *currentRoots = @[self.lastSnapshot ?: [self fb_customSnapshot]];
|
|
140
|
+
NSArray<id<FBXCElementSnapshot>> *currentRoots = @[self.lastSnapshot ?: self.fb_cachedSnapshot ?: [self fb_customSnapshot]];
|
|
141
141
|
FBClassChainItem *chainItem = lookupChain.firstObject;
|
|
142
142
|
NSArray<id<FBXCElementSnapshot>> *candidates = [self.class fb_snapshotsMatchingItem:chainItem inRoots:currentRoots];
|
|
143
143
|
[lookupChain removeObjectAtIndex:0];
|
|
@@ -36,12 +36,13 @@ int const MAX_ITERATIONS_COUNT = 100;
|
|
|
36
36
|
|
|
37
37
|
FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:self];
|
|
38
38
|
for (int i = 0; i < MAX_ITERATIONS_COUNT; i++) {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
FBTVDirection direction = FBTVDirectionNone;
|
|
40
|
+
FBTVFocusState focusState = [tracker pollFocusState:&direction];
|
|
41
|
+
if (focusState == FBTVFocusStateFocused) {
|
|
41
42
|
return YES;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
if (
|
|
45
|
+
if (focusState == FBTVFocusStateGone) {
|
|
45
46
|
if (error) {
|
|
46
47
|
*error = [[FBErrorBuilder.builder withDescription:
|
|
47
48
|
[NSString stringWithFormat:@"'%@' element is not reachable because it does not exist. Try to use XCUIRemote commands.", self.description]] build];
|
|
@@ -49,7 +50,6 @@ int const MAX_ITERATIONS_COUNT = 100;
|
|
|
49
50
|
return NO;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
FBTVDirection direction = tracker.directionToFocusedElement;
|
|
53
53
|
if (direction != FBTVDirectionNone) {
|
|
54
54
|
[[XCUIRemote sharedRemote] pressButton: (XCUIRemoteButton)direction];
|
|
55
55
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
#import <XCTest/XCTest.h>
|
|
10
|
+
#import <WebDriverAgentLib/FBXCElementSnapshot.h>
|
|
10
11
|
|
|
11
12
|
NS_ASSUME_NONNULL_BEGIN
|
|
12
13
|
|
|
@@ -50,6 +51,25 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error);
|
|
|
50
51
|
frequency:(NSUInteger)frequency
|
|
51
52
|
error:(NSError **)error;
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
Types a text into element, reusing a snapshot the caller already holds
|
|
56
|
+
instead of fetching a new one. Only pass a snapshot that is known to
|
|
57
|
+
reflect the element's current state (e.g. one taken moments earlier in
|
|
58
|
+
the same operation) - a stale snapshot here will not be detected as such.
|
|
59
|
+
|
|
60
|
+
@param text text that should be typed
|
|
61
|
+
@param shouldClear Whether to clear the input field before start typing
|
|
62
|
+
@param frequency Frequency of typing (letters per sec)
|
|
63
|
+
@param snapshot A recent snapshot of the receiver
|
|
64
|
+
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
|
65
|
+
@return YES if the operation succeeds, otherwise NO.
|
|
66
|
+
*/
|
|
67
|
+
- (BOOL)fb_typeText:(NSString *)text
|
|
68
|
+
shouldClear:(BOOL)shouldClear
|
|
69
|
+
frequency:(NSUInteger)frequency
|
|
70
|
+
snapshot:(id<FBXCElementSnapshot>)snapshot
|
|
71
|
+
error:(NSError **)error;
|
|
72
|
+
|
|
53
73
|
/**
|
|
54
74
|
Clears text on element.
|
|
55
75
|
It will try to activate keyboard on element, if element has no keyboard focus.
|
|
@@ -59,6 +79,19 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error);
|
|
|
59
79
|
*/
|
|
60
80
|
- (BOOL)fb_clearTextWithError:(NSError **)error;
|
|
61
81
|
|
|
82
|
+
/**
|
|
83
|
+
Clears text on element, reusing a snapshot the caller already holds
|
|
84
|
+
instead of fetching a new one. Only pass a snapshot that is known to
|
|
85
|
+
reflect the element's current state (e.g. one taken moments earlier in
|
|
86
|
+
the same operation) - a stale snapshot here will not be detected as such.
|
|
87
|
+
|
|
88
|
+
@param snapshot A recent snapshot of the receiver
|
|
89
|
+
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
|
90
|
+
@return YES if the operation succeeds, otherwise NO.
|
|
91
|
+
*/
|
|
92
|
+
- (BOOL)fb_clearTextWithSnapshot:(id<FBXCElementSnapshot>)snapshot
|
|
93
|
+
error:(NSError **)error;
|
|
94
|
+
|
|
62
95
|
@end
|
|
63
96
|
|
|
64
97
|
NS_ASSUME_NONNULL_END
|
|
@@ -109,7 +109,22 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
|
|
|
109
109
|
frequency:(NSUInteger)frequency
|
|
110
110
|
error:(NSError **)error
|
|
111
111
|
{
|
|
112
|
+
// No snapshot was handed to us, so there is nothing to safely reuse -
|
|
113
|
+
// always fetch a fresh one.
|
|
112
114
|
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
|
115
|
+
return [self fb_typeText:text
|
|
116
|
+
shouldClear:shouldClear
|
|
117
|
+
frequency:frequency
|
|
118
|
+
snapshot:snapshot
|
|
119
|
+
error:error];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
- (BOOL)fb_typeText:(NSString *)text
|
|
123
|
+
shouldClear:(BOOL)shouldClear
|
|
124
|
+
frequency:(NSUInteger)frequency
|
|
125
|
+
snapshot:(id<FBXCElementSnapshot>)snapshot
|
|
126
|
+
error:(NSError **)error
|
|
127
|
+
{
|
|
113
128
|
FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
|
114
129
|
[self fb_prepareForTextInputWithSnapshot:wrapped];
|
|
115
130
|
if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) {
|
|
@@ -120,7 +135,15 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
|
|
|
120
135
|
|
|
121
136
|
- (BOOL)fb_clearTextWithError:(NSError **)error
|
|
122
137
|
{
|
|
138
|
+
// No snapshot was handed to us, so there is nothing to safely reuse -
|
|
139
|
+
// always fetch a fresh one.
|
|
123
140
|
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
|
141
|
+
return [self fb_clearTextWithSnapshot:snapshot error:error];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
- (BOOL)fb_clearTextWithSnapshot:(id<FBXCElementSnapshot>)snapshot
|
|
145
|
+
error:(NSError **)error
|
|
146
|
+
{
|
|
124
147
|
return [self fb_clearTextWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot]
|
|
125
148
|
shouldPrepareForInput:YES
|
|
126
149
|
error:error];
|
|
@@ -221,9 +221,14 @@
|
|
|
221
221
|
}
|
|
222
222
|
NSUInteger frequency = (NSUInteger)[request.arguments[@"frequency"] longLongValue] ?: [FBConfiguration maxTypingFrequency];
|
|
223
223
|
NSError *error = nil;
|
|
224
|
+
// checkStaleness:YES above already took a fresh, verified-live snapshot of
|
|
225
|
+
// `element` and cached it as `lastSnapshot` - reuse it instead of paying
|
|
226
|
+
// for another round trip.
|
|
227
|
+
id<FBXCElementSnapshot> snapshot = element.lastSnapshot ?: element.fb_cachedSnapshot ?: [element fb_standardSnapshot];
|
|
224
228
|
if (![element fb_typeText:textToType
|
|
225
229
|
shouldClear:NO
|
|
226
230
|
frequency:frequency
|
|
231
|
+
snapshot:snapshot
|
|
227
232
|
error:&error]) {
|
|
228
233
|
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
|
229
234
|
}
|
|
@@ -346,15 +351,15 @@
|
|
|
346
351
|
|
|
347
352
|
+ (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
|
|
348
353
|
{
|
|
349
|
-
|
|
354
|
+
XCUIApplication *application = request.session.activeApplication;
|
|
350
355
|
CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
|
|
351
356
|
(CGFloat)[request.arguments[@"fromY"] doubleValue]);
|
|
352
357
|
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
|
|
353
|
-
element:
|
|
358
|
+
element:application];
|
|
354
359
|
CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
|
|
355
360
|
(CGFloat)[request.arguments[@"toY"] doubleValue]);
|
|
356
361
|
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
|
|
357
|
-
element:
|
|
362
|
+
element:application];
|
|
358
363
|
[startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
|
|
359
364
|
thenDragToCoordinate:endCoordinate
|
|
360
365
|
withVelocity:[request.arguments[@"velocity"] doubleValue]
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
XCUIElement *alertElement = self.alertElement;
|
|
75
75
|
self.cachedAlertSnapshot = nil == alertElement
|
|
76
76
|
? nil
|
|
77
|
-
: (alertElement.
|
|
77
|
+
: (alertElement.fb_cachedSnapshot ?: [alertElement fb_customSnapshot]);
|
|
78
78
|
self.hasCachedAlertSnapshot = YES;
|
|
79
79
|
}
|
|
80
80
|
return self.cachedAlertSnapshot;
|
|
@@ -194,7 +194,15 @@
|
|
|
194
194
|
[self fb_raiseSetTextFailedExceptionWithReason:@"Failed to resolve the input field element"];
|
|
195
195
|
}
|
|
196
196
|
NSError *error;
|
|
197
|
-
|
|
197
|
+
// dstField was just resolved via a live query in fb_elementForSnapshot:underElement:
|
|
198
|
+
// above, so its cached snapshot can be safely reconstructed in memory
|
|
199
|
+
// instead of paying for a fresh round trip.
|
|
200
|
+
id<FBXCElementSnapshot> snapshot = dstField.fb_cachedSnapshot ?: [dstField fb_standardSnapshot];
|
|
201
|
+
if (![dstField fb_typeText:text
|
|
202
|
+
shouldClear:YES
|
|
203
|
+
frequency:FBConfiguration.maxTypingFrequency
|
|
204
|
+
snapshot:snapshot
|
|
205
|
+
error:&error]) {
|
|
198
206
|
[self fb_raiseSetTextFailedExceptionWithReason:error.description];
|
|
199
207
|
}
|
|
200
208
|
}
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.1.
|
|
18
|
+
<string>16.1.5</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.1.
|
|
22
|
+
<string>16.1.5</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
#import <Foundation/Foundation.h>
|
|
10
|
+
#import "FBXCElementSnapshot.h"
|
|
10
11
|
|
|
11
12
|
#if TARGET_OS_TV
|
|
12
13
|
|
|
@@ -22,6 +23,15 @@
|
|
|
22
23
|
|
|
23
24
|
- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
|
|
24
25
|
- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
|
|
26
|
+
- (FBTVDirection)directionWithItem:(FBTVNavigationItem *)item
|
|
27
|
+
delta:(CGFloat)delta
|
|
28
|
+
positiveDirection:(FBTVDirection)positiveDirection
|
|
29
|
+
negativeDirection:(FBTVDirection)negativeDirection;
|
|
30
|
+
|
|
31
|
+
// Exposed for testing: the pure direction-math core of `-pollFocusState:`,
|
|
32
|
+
// parameterized on the already-resolved focused element's snapshot so tests
|
|
33
|
+
// can supply a fake one instead of a live query result.
|
|
34
|
+
- (FBTVDirection)directionTowardsTargetFromFocusedElementSnapshot:(id<FBXCElementSnapshot>)focusedSnapshot;
|
|
25
35
|
@end
|
|
26
36
|
|
|
27
37
|
#endif
|
|
@@ -22,6 +22,19 @@ typedef NS_ENUM(NSUInteger, FBTVDirection) {
|
|
|
22
22
|
FBTVDirectionNone = 4
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
Represents where the tracked target element currently stands with respect
|
|
27
|
+
to keyboard/remote focus, as of the last `-pollFocusState:` call.
|
|
28
|
+
*/
|
|
29
|
+
typedef NS_ENUM(NSUInteger, FBTVFocusState) {
|
|
30
|
+
// The target element currently has focus
|
|
31
|
+
FBTVFocusStateFocused,
|
|
32
|
+
// The target element could not be located in the accessibility tree anymore
|
|
33
|
+
FBTVFocusStateGone,
|
|
34
|
+
// The target element exists, but does not have focus yet
|
|
35
|
+
FBTVFocusStatePending,
|
|
36
|
+
};
|
|
37
|
+
|
|
25
38
|
NS_ASSUME_NONNULL_BEGIN
|
|
26
39
|
|
|
27
40
|
@interface FBTVNavigationItem : NSObject
|
|
@@ -38,12 +51,24 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
38
51
|
+ (instancetype)trackerWithTargetElement: (XCUIElement *) targetElement;
|
|
39
52
|
|
|
40
53
|
/**
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
Determines whether the tracked target element currently has focus, whether
|
|
55
|
+
it still exists, and - if neither - which direction the focus should move
|
|
56
|
+
to get closer to it. Each of these is resolved with the cheapest available
|
|
57
|
+
accessibility round trip for what it's checking: `hasFocus`/`exists` are
|
|
58
|
+
single already-known-element checks, and the currently focused element (if
|
|
59
|
+
any) is found via a targeted `hasFocus == true` query rather than a whole-app
|
|
60
|
+
snapshot walk, whose cost scales with total app size/depth instead of with
|
|
61
|
+
the (typically 0-1) number of matches.
|
|
43
62
|
|
|
44
|
-
@
|
|
63
|
+
@param direction Always reset to `FBTVDirectionNone` first, then set to the
|
|
64
|
+
suggested direction to move the focus to when the return value is
|
|
65
|
+
`FBTVFocusStatePending`. It is left as `FBTVDirectionNone` when the return
|
|
66
|
+
value is `FBTVFocusStateFocused`/`FBTVFocusStateGone`, or when the
|
|
67
|
+
currently focused element could not be determined yet - in the latter
|
|
68
|
+
case the caller should just retry on the next iteration.
|
|
69
|
+
@return The current focus state of the tracked target element
|
|
45
70
|
*/
|
|
46
|
-
- (FBTVDirection)
|
|
71
|
+
- (FBTVFocusState)pollFocusState:(FBTVDirection *)direction;
|
|
47
72
|
|
|
48
73
|
@end
|
|
49
74
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#import "FBTVNavigationTracker-Private.h"
|
|
11
11
|
|
|
12
12
|
#import "FBMathUtils.h"
|
|
13
|
+
#import "FBXCElementSnapshotWrapper.h"
|
|
13
14
|
#import "XCUIElement+FBCaching.h"
|
|
14
15
|
#import "XCUIElement+FBUtilities.h"
|
|
15
16
|
#import "XCUIElement+FBWebDriverAttributes.h"
|
|
@@ -56,17 +57,49 @@
|
|
|
56
57
|
self = [super init];
|
|
57
58
|
if (self) {
|
|
58
59
|
_targetElement = targetElement;
|
|
59
|
-
|
|
60
|
-
_targetCenter = FBRectGetCenter(frame);
|
|
60
|
+
_targetCenter = FBRectGetCenter(targetElement.wdFrame);
|
|
61
61
|
_navigationItems = [NSMutableDictionary dictionary];
|
|
62
62
|
}
|
|
63
63
|
return self;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
- (FBTVDirection)
|
|
66
|
+
- (FBTVFocusState)pollFocusState:(FBTVDirection *)direction
|
|
67
67
|
{
|
|
68
|
-
|
|
68
|
+
*direction = FBTVDirectionNone;
|
|
69
69
|
|
|
70
|
+
// `hasFocus`/`exists` are cheap, single-element live checks - each round
|
|
71
|
+
// trip only has to serialize the state of one already-known element, not
|
|
72
|
+
// an unbounded subtree, so there's no snapshot-walk win to be had here.
|
|
73
|
+
if (self.targetElement.hasFocus) {
|
|
74
|
+
return FBTVFocusStateFocused;
|
|
75
|
+
}
|
|
76
|
+
if (!self.targetElement.exists) {
|
|
77
|
+
return FBTVFocusStateGone;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Likewise, a `hasFocus == true` predicate query only has to serialize its
|
|
81
|
+
// (typically 0-1) matches back over the round trip, unlike a whole-app
|
|
82
|
+
// snapshot walk, whose cost scales with total app size/depth.
|
|
83
|
+
XCUIElement *focusedElement = (XCUIElement *)XCUIApplication.fb_activeApplication.fb_focusedElement;
|
|
84
|
+
if (nil == focusedElement) {
|
|
85
|
+
// Nothing is focused yet (e.g. right after the app became active) - let
|
|
86
|
+
// the caller retry on the next iteration.
|
|
87
|
+
return FBTVFocusStatePending;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The snapshot was already taken as a side effect of resolving the query
|
|
91
|
+
// above, so grab it for free instead of paying for a new round trip.
|
|
92
|
+
id<FBXCElementSnapshot> focusedSnapshot = focusedElement.lastSnapshot
|
|
93
|
+
?: focusedElement.fb_cachedSnapshot
|
|
94
|
+
?: [focusedElement fb_customSnapshot];
|
|
95
|
+
*direction = [self directionTowardsTargetFromFocusedElementSnapshot:focusedSnapshot];
|
|
96
|
+
|
|
97
|
+
return FBTVFocusStatePending;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (FBTVDirection)directionTowardsTargetFromFocusedElementSnapshot:(id<FBXCElementSnapshot>)focusedSnapshot
|
|
101
|
+
{
|
|
102
|
+
FBXCElementSnapshotWrapper *focused = [FBXCElementSnapshotWrapper ensureWrapped:focusedSnapshot];
|
|
70
103
|
CGPoint focusedCenter = FBRectGetCenter(focused.wdFrame);
|
|
71
104
|
FBTVNavigationItem *item = [self navigationItemWithElement:focused];
|
|
72
105
|
CGFloat yDelta = self.targetCenter.y - focusedCenter.y;
|
|
@@ -83,7 +116,6 @@
|
|
|
83
116
|
direction = [self verticalDirectionWithItem:item andDelta:yDelta];
|
|
84
117
|
}
|
|
85
118
|
}
|
|
86
|
-
|
|
87
119
|
return direction;
|
|
88
120
|
}
|
|
89
121
|
|
|
@@ -99,7 +131,7 @@
|
|
|
99
131
|
if (nil != item) {
|
|
100
132
|
return item;
|
|
101
133
|
}
|
|
102
|
-
|
|
134
|
+
|
|
103
135
|
item = [FBTVNavigationItem itemWithUid:uid];
|
|
104
136
|
[self.navigationItems setObject:item forKey:uid];
|
|
105
137
|
return item;
|
|
@@ -107,32 +139,35 @@
|
|
|
107
139
|
|
|
108
140
|
- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
|
|
109
141
|
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return FBTVDirectionRight;
|
|
115
|
-
}
|
|
116
|
-
if (delta < -DBL_EPSILON &&
|
|
117
|
-
![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionLeft]]) {
|
|
118
|
-
[item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionLeft]];
|
|
119
|
-
return FBTVDirectionLeft;
|
|
120
|
-
}
|
|
121
|
-
return FBTVDirectionNone;
|
|
142
|
+
return [self directionWithItem:item
|
|
143
|
+
delta:delta
|
|
144
|
+
positiveDirection:FBTVDirectionRight
|
|
145
|
+
negativeDirection:FBTVDirectionLeft];
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
|
|
125
149
|
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
return [self directionWithItem:item
|
|
151
|
+
delta:delta
|
|
152
|
+
positiveDirection:FBTVDirectionDown
|
|
153
|
+
negativeDirection:FBTVDirectionUp];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
- (FBTVDirection)directionWithItem:(FBTVNavigationItem *)item
|
|
157
|
+
delta:(CGFloat)delta
|
|
158
|
+
positiveDirection:(FBTVDirection)positiveDirection
|
|
159
|
+
negativeDirection:(FBTVDirection)negativeDirection
|
|
160
|
+
{
|
|
161
|
+
// CGFloat is double in 64bit. tvOS is only for arm64
|
|
162
|
+
NSNumber *positiveDirectionNumber = [NSNumber numberWithInteger:positiveDirection];
|
|
163
|
+
NSNumber *negativeDirectionNumber = [NSNumber numberWithInteger:negativeDirection];
|
|
164
|
+
if (delta > DBL_EPSILON && ![item.directions containsObject:positiveDirectionNumber]) {
|
|
165
|
+
[item.directions addObject:positiveDirectionNumber];
|
|
166
|
+
return positiveDirection;
|
|
131
167
|
}
|
|
132
|
-
if (delta < -DBL_EPSILON &&
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return FBTVDirectionUp;
|
|
168
|
+
if (delta < -DBL_EPSILON && ![item.directions containsObject:negativeDirectionNumber]) {
|
|
169
|
+
[item.directions addObject:negativeDirectionNumber];
|
|
170
|
+
return negativeDirection;
|
|
136
171
|
}
|
|
137
172
|
return FBTVDirectionNone;
|
|
138
173
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
|
12
|
+
|
|
13
|
+
@end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import "AppDelegate.h"
|
|
10
|
+
#import "SceneDelegate.h"
|
|
11
|
+
|
|
12
|
+
@implementation AppDelegate
|
|
13
|
+
|
|
14
|
+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
|
15
|
+
{
|
|
16
|
+
return YES;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options
|
|
20
|
+
{
|
|
21
|
+
UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
|
|
22
|
+
configuration.delegateClass = SceneDelegate.class;
|
|
23
|
+
return configuration;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>en</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
11
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
+
<string>6.0</string>
|
|
13
|
+
<key>CFBundleName</key>
|
|
14
|
+
<string>$(PRODUCT_NAME)</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>APPL</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>1.0</string>
|
|
19
|
+
<key>CFBundleSignature</key>
|
|
20
|
+
<string>????</string>
|
|
21
|
+
<key>CFBundleVersion</key>
|
|
22
|
+
<string>1</string>
|
|
23
|
+
<key>UILaunchScreen</key>
|
|
24
|
+
<dict/>
|
|
25
|
+
<key>UIApplicationSceneManifest</key>
|
|
26
|
+
<dict>
|
|
27
|
+
<key>UIApplicationSupportsMultipleScenes</key>
|
|
28
|
+
<false/>
|
|
29
|
+
<key>UISceneConfigurations</key>
|
|
30
|
+
<dict>
|
|
31
|
+
<key>UIWindowSceneSessionRoleApplication</key>
|
|
32
|
+
<array>
|
|
33
|
+
<dict>
|
|
34
|
+
<key>UISceneConfigurationName</key>
|
|
35
|
+
<string>Default Configuration</string>
|
|
36
|
+
</dict>
|
|
37
|
+
</array>
|
|
38
|
+
</dict>
|
|
39
|
+
</dict>
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
|
|
12
|
+
|
|
13
|
+
@property (strong, nonatomic) UIWindow *window;
|
|
14
|
+
|
|
15
|
+
@end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import "SceneDelegate.h"
|
|
10
|
+
#import "ViewController.h"
|
|
11
|
+
|
|
12
|
+
@implementation SceneDelegate
|
|
13
|
+
|
|
14
|
+
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions
|
|
15
|
+
{
|
|
16
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
17
|
+
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
|
|
18
|
+
self.window.rootViewController = [ViewController new];
|
|
19
|
+
[self.window makeKeyAndVisible];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
@interface ViewController : UIViewController
|
|
12
|
+
|
|
13
|
+
@end
|