appium-webdriveragent 16.1.3 → 16.1.4
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 +7 -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/XCUIElement+FBTVFocuse.m +4 -4
- package/WebDriverAgentLib/Commands/FBElementCommands.m +3 -3
- 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>
|
|
@@ -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
|
}
|
|
@@ -346,15 +346,15 @@
|
|
|
346
346
|
|
|
347
347
|
+ (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
|
|
348
348
|
{
|
|
349
|
-
|
|
349
|
+
XCUIApplication *application = request.session.activeApplication;
|
|
350
350
|
CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
|
|
351
351
|
(CGFloat)[request.arguments[@"fromY"] doubleValue]);
|
|
352
352
|
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
|
|
353
|
-
element:
|
|
353
|
+
element:application];
|
|
354
354
|
CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
|
|
355
355
|
(CGFloat)[request.arguments[@"toY"] doubleValue]);
|
|
356
356
|
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
|
|
357
|
-
element:
|
|
357
|
+
element:application];
|
|
358
358
|
[startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
|
|
359
359
|
thenDragToCoordinate:endCoordinate
|
|
360
360
|
withVelocity:[request.arguments[@"velocity"] doubleValue]
|
|
@@ -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.4</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.1.
|
|
22
|
+
<string>16.1.4</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
|
|
@@ -0,0 +1,79 @@
|
|
|
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 "ViewController.h"
|
|
10
|
+
|
|
11
|
+
static NSInteger const FBGridSize = 3;
|
|
12
|
+
static CGFloat const FBCellSide = 200;
|
|
13
|
+
static CGFloat const FBCellSpacing = 60;
|
|
14
|
+
|
|
15
|
+
@interface ViewController ()
|
|
16
|
+
@property (nonatomic, strong) UILabel *lastSelectedLabel;
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
@implementation ViewController
|
|
20
|
+
|
|
21
|
+
- (void)viewDidLoad
|
|
22
|
+
{
|
|
23
|
+
[super viewDidLoad];
|
|
24
|
+
self.view.backgroundColor = UIColor.whiteColor;
|
|
25
|
+
|
|
26
|
+
UIStackView *rows = [UIStackView new];
|
|
27
|
+
rows.axis = UILayoutConstraintAxisVertical;
|
|
28
|
+
rows.spacing = FBCellSpacing;
|
|
29
|
+
rows.alignment = UIStackViewAlignmentCenter;
|
|
30
|
+
rows.translatesAutoresizingMaskIntoConstraints = NO;
|
|
31
|
+
[self.view addSubview:rows];
|
|
32
|
+
|
|
33
|
+
for (NSInteger row = 0; row < FBGridSize; row++) {
|
|
34
|
+
UIStackView *columns = [UIStackView new];
|
|
35
|
+
columns.axis = UILayoutConstraintAxisHorizontal;
|
|
36
|
+
columns.spacing = FBCellSpacing;
|
|
37
|
+
for (NSInteger column = 0; column < FBGridSize; column++) {
|
|
38
|
+
UIButton *cell = [self newCellWithIdentifier:[NSString stringWithFormat:@"cell_%ld_%ld", (long)row, (long)column]];
|
|
39
|
+
[columns addArrangedSubview:cell];
|
|
40
|
+
}
|
|
41
|
+
[rows addArrangedSubview:columns];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
UIButton *disabledButton = [self newCellWithIdentifier:@"disabledButton"];
|
|
45
|
+
disabledButton.enabled = NO;
|
|
46
|
+
[rows addArrangedSubview:disabledButton];
|
|
47
|
+
|
|
48
|
+
self.lastSelectedLabel = [UILabel new];
|
|
49
|
+
self.lastSelectedLabel.accessibilityIdentifier = @"lastSelectedLabel";
|
|
50
|
+
self.lastSelectedLabel.text = @"none";
|
|
51
|
+
self.lastSelectedLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
52
|
+
[self.view addSubview:self.lastSelectedLabel];
|
|
53
|
+
|
|
54
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
55
|
+
[rows.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
|
|
56
|
+
[rows.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
|
|
57
|
+
[self.lastSelectedLabel.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
|
|
58
|
+
[self.lastSelectedLabel.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-FBCellSpacing],
|
|
59
|
+
]];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (UIButton *)newCellWithIdentifier:(NSString *)identifier
|
|
63
|
+
{
|
|
64
|
+
UIButton *cell = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
65
|
+
cell.accessibilityIdentifier = identifier;
|
|
66
|
+
[cell setTitle:identifier forState:UIControlStateNormal];
|
|
67
|
+
cell.translatesAutoresizingMaskIntoConstraints = NO;
|
|
68
|
+
[cell.widthAnchor constraintEqualToConstant:FBCellSide].active = YES;
|
|
69
|
+
[cell.heightAnchor constraintEqualToConstant:FBCellSide].active = YES;
|
|
70
|
+
[cell addTarget:self action:@selector(didSelectCell:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
71
|
+
return cell;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)didSelectCell:(UIButton *)cell
|
|
75
|
+
{
|
|
76
|
+
self.lastSelectedLabel.text = cell.accessibilityIdentifier;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@end
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
#import "AppDelegate.h"
|
|
12
|
+
|
|
13
|
+
int main(int argc, char * argv[]) {
|
|
14
|
+
@autoreleasepool {
|
|
15
|
+
return UIApplicationMain(argc, argv, nil, NSStringFromClass(AppDelegate.class));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 <XCTest/XCTest.h>
|
|
10
|
+
|
|
11
|
+
#if TARGET_OS_TV
|
|
12
|
+
|
|
13
|
+
// `XCUIElement+FBTVFocuse` is a project-internal category (not exposed as a
|
|
14
|
+
// Public framework header), so its two entry points are forward-declared
|
|
15
|
+
// here rather than imported. The real implementation - the one this suite
|
|
16
|
+
// exercises end to end against a live tvOS Simulator - is still the one
|
|
17
|
+
// linked in from WebDriverAgentLib_tvOS.framework.
|
|
18
|
+
@interface XCUIElement (FBTVFocuseForTesting)
|
|
19
|
+
- (BOOL)fb_setFocusWithError:(NSError **)error;
|
|
20
|
+
- (BOOL)fb_selectWithError:(NSError **)error;
|
|
21
|
+
@end
|
|
22
|
+
|
|
23
|
+
@interface FBTVFocusIntegrationTests : XCTestCase
|
|
24
|
+
@property (nonatomic, strong) XCUIApplication *app;
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation FBTVFocusIntegrationTests
|
|
28
|
+
|
|
29
|
+
- (void)setUp
|
|
30
|
+
{
|
|
31
|
+
[super setUp];
|
|
32
|
+
self.continueAfterFailure = NO;
|
|
33
|
+
self.app = [XCUIApplication new];
|
|
34
|
+
[self.app launch];
|
|
35
|
+
XCTAssertTrue([self.app.buttons[@"cell_0_0"] waitForExistenceWithTimeout:15]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (void)testSetFocusMovesFocusToADistantElement
|
|
39
|
+
{
|
|
40
|
+
XCUIElement *target = self.app.buttons[@"cell_2_2"];
|
|
41
|
+
XCTAssertTrue(target.exists);
|
|
42
|
+
XCTAssertFalse(target.hasFocus);
|
|
43
|
+
|
|
44
|
+
NSError *error;
|
|
45
|
+
BOOL result = [target fb_setFocusWithError:&error];
|
|
46
|
+
|
|
47
|
+
XCTAssertTrue(result);
|
|
48
|
+
XCTAssertNil(error);
|
|
49
|
+
XCTAssertTrue(target.hasFocus);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)testSelectMovesFocusAndTriggersTheElementsAction
|
|
53
|
+
{
|
|
54
|
+
XCUIElement *target = self.app.buttons[@"cell_1_2"];
|
|
55
|
+
XCUIElement *statusLabel = self.app.staticTexts[@"lastSelectedLabel"];
|
|
56
|
+
|
|
57
|
+
NSError *error;
|
|
58
|
+
BOOL result = [target fb_selectWithError:&error];
|
|
59
|
+
|
|
60
|
+
XCTAssertTrue(result);
|
|
61
|
+
XCTAssertNil(error);
|
|
62
|
+
XCTAssertTrue(target.hasFocus);
|
|
63
|
+
|
|
64
|
+
NSPredicate *labelUpdated = [NSPredicate predicateWithFormat:@"label == %@", @"cell_1_2"];
|
|
65
|
+
XCTNSPredicateExpectation *expectation = [[XCTNSPredicateExpectation alloc] initWithPredicate:labelUpdated object:statusLabel];
|
|
66
|
+
[self waitForExpectations:@[expectation] timeout:5];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)testSetFocusOnADisabledElementFails
|
|
70
|
+
{
|
|
71
|
+
XCUIElement *disabled = self.app.buttons[@"disabledButton"];
|
|
72
|
+
XCTAssertTrue(disabled.exists);
|
|
73
|
+
|
|
74
|
+
NSError *error;
|
|
75
|
+
BOOL result = [disabled fb_setFocusWithError:&error];
|
|
76
|
+
|
|
77
|
+
XCTAssertFalse(result);
|
|
78
|
+
XCTAssertNotNil(error);
|
|
79
|
+
XCTAssertFalse(disabled.hasFocus);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@end
|
|
83
|
+
|
|
84
|
+
#endif
|