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.
Files changed (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/WebDriverAgent.xcodeproj/project.pbxproj +297 -0
  3. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_tvOS.xcscheme +78 -0
  4. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_tvOS.xcscheme +86 -0
  5. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +2 -2
  6. package/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m +1 -1
  7. package/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +4 -4
  8. package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +33 -0
  9. package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +23 -0
  10. package/WebDriverAgentLib/Commands/FBElementCommands.m +8 -3
  11. package/WebDriverAgentLib/FBAlert.m +10 -2
  12. package/WebDriverAgentLib/Info.plist +2 -2
  13. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +10 -0
  14. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +29 -4
  15. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +62 -27
  16. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.h +13 -0
  17. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.m +26 -0
  18. package/WebDriverAgentTests/IntegrationApp_tvOS/Info.plist +41 -0
  19. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.h +15 -0
  20. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.m +22 -0
  21. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.h +13 -0
  22. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.m +79 -0
  23. package/WebDriverAgentTests/IntegrationApp_tvOS/main.m +17 -0
  24. package/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +84 -0
  25. package/WebDriverAgentTests/IntegrationTests_tvOS/Info.plist +24 -0
  26. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.h +27 -0
  27. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.m +23 -0
  28. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.h +43 -0
  29. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.m +24 -0
  30. package/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m +48 -0
  31. 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.lastSnapshot ?: sheet.fb_cachedSnapshot ?: [sheet fb_customSnapshot];
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.lastSnapshot ?: scrollView.fb_cachedSnapshot ?: [scrollView fb_customSnapshot];
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
- // Here hasFocus works so far. Maybe, it is because it is handled by `XCUIRemote`...
40
- if (self.hasFocus) {
39
+ FBTVDirection direction = FBTVDirectionNone;
40
+ FBTVFocusState focusState = [tracker pollFocusState:&direction];
41
+ if (focusState == FBTVFocusStateFocused) {
41
42
  return YES;
42
43
  }
43
44
 
44
- if (!self.exists) {
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
- FBSession *session = request.session;
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:session.activeApplication];
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:session.activeApplication];
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.lastSnapshot ?: alertElement.fb_cachedSnapshot ?: [alertElement fb_customSnapshot]);
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
- if (![dstField fb_typeText:text shouldClear:YES error:&error]) {
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.3</string>
18
+ <string>16.1.5</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.1.3</string>
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
- Determine the correct direction to move the focus to the tracked target
42
- element from the currently focused one
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
- @return FBTVDirection to move the focus to
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)directionToFocusedElement;
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
- CGRect frame = targetElement.wdFrame;
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)directionToFocusedElement
66
+ - (FBTVFocusState)pollFocusState:(FBTVDirection *)direction
67
67
  {
68
- XCUIElement *focused = XCUIApplication.fb_activeApplication.fb_focusedElement;
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
- // GCFloat is double in 64bit. tvOS is only for arm64
111
- if (delta > DBL_EPSILON &&
112
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionRight]]) {
113
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionRight]];
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
- // GCFloat is double in 64bit. tvOS is only for arm64
127
- if (delta > DBL_EPSILON &&
128
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionDown]]) {
129
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionDown]];
130
- return FBTVDirectionDown;
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
- ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionUp]]) {
134
- [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionUp]];
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