From d2f980638414f5abec0a156560d8e67d029826a7 Mon Sep 17 00:00:00 2001 From: ziin Date: Tue, 3 Feb 2026 16:52:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1360 +++ CONTRIBUTING.md | 35 + Configurations/IOSSettings.xcconfig | 31 + Configurations/IOSTestSettings.xcconfig | 1 + Configurations/TVOSSettings.xcconfig | 31 + Configurations/TVOSTestSettings.xcconfig | 1 + Fastlane/Fastfile | 13 + Gemfile | 3 + LICENSE | 30 + .../AccessibilityUtilities/AXSettings.h | 15 + .../LSApplicationWorkspace.h | 127 + .../TextInput/TIPreferencesController.h | 45 + PrivateHeaders/UIKitCore/UIKeyboardImpl.h | 27 + PrivateHeaders/XCTest/CDStructures.h | 28 + PrivateHeaders/XCTest/NSString-XCTAdditions.h | 11 + .../XCTest/NSValue-XCTestAdditions.h | 11 + .../UIGestureRecognizer-RecordingAdditions.h | 11 + ...ressGestureRecognizer-RecordingAdditions.h | 11 + ...IPanGestureRecognizer-RecordingAdditions.h | 11 + ...inchGestureRecognizer-RecordingAdditions.h | 11 + ...wipeGestureRecognizer-RecordingAdditions.h | 11 + ...ITapGestureRecognizer-RecordingAdditions.h | 11 + PrivateHeaders/XCTest/XCAXClient_iOS.h | 55 + PrivateHeaders/XCTest/XCActivityRecord.h | 39 + PrivateHeaders/XCTest/XCApplicationMonitor.h | 44 + .../XCTest/XCApplicationMonitor_iOS.h | 18 + PrivateHeaders/XCTest/XCApplicationQuery.h | 23 + .../XCTest/XCDebugLogDelegate-Protocol.h | 11 + PrivateHeaders/XCTest/XCDeviceEvent.h | 27 + PrivateHeaders/XCTest/XCEventGenerator.h | 73 + PrivateHeaders/XCTest/XCKeyMappingPath.h | 36 + PrivateHeaders/XCTest/XCKeyboardInputSolver.h | 41 + PrivateHeaders/XCTest/XCKeyboardKeyMap.h | 101 + PrivateHeaders/XCTest/XCKeyboardLayout.h | 35 + PrivateHeaders/XCTest/XCPointerEvent.h | 30 + PrivateHeaders/XCTest/XCPointerEventPath.h | 43 + PrivateHeaders/XCTest/XCSourceCodeRecording.h | 42 + PrivateHeaders/XCTest/XCSourceCodeTreeNode.h | 84 + .../XCTest/XCSourceCodeTreeNodeEnumerator.h | 18 + PrivateHeaders/XCTest/XCSymbolicationRecord.h | 29 + PrivateHeaders/XCTest/XCSymbolicatorHolder.h | 14 + .../XCTest/XCSynthesizedEventRecord.h | 32 + PrivateHeaders/XCTest/XCTAXClient-Protocol.h | 14 + .../XCTest/XCTAsyncActivity-Protocol.h | 16 + PrivateHeaders/XCTest/XCTAsyncActivity.h | 23 + .../XCTest/XCTAutomationTarget-Protocol.h | 12 + .../XCTest/XCTDarwinNotificationExpectation.h | 23 + .../XCTElementSetTransformer-Protocol.h | 18 + PrivateHeaders/XCTest/XCTKVOExpectation.h | 28 + PrivateHeaders/XCTest/XCTMetric.h | 29 + .../XCTest/XCTNSNotificationExpectation.h | 27 + .../XCTest/XCTNSPredicateExpectation.h | 24 + ...XCTNSPredicateExpectationObject-Protocol.h | 15 + .../XCTest/XCTRunnerAutomationSession.h | 21 + .../XCTest/XCTRunnerDaemonSession.h | 98 + PrivateHeaders/XCTest/XCTRunnerIDESession.h | 65 + PrivateHeaders/XCTest/XCTTestRunSession.h | 25 + .../XCTTestRunSessionDelegate-Protocol.h | 17 + .../XCTest/XCTUIApplicationMonitor-Protocol.h | 17 + PrivateHeaders/XCTest/XCTWaiter.h | 55 + .../XCTest/XCTWaiterDelegate-Protocol.h | 16 + .../XCTWaiterDelegatePrivate-Protocol.h | 12 + .../XCTest/XCTWaiterManagement-Protocol.h | 13 + PrivateHeaders/XCTest/XCTWaiterManager.h | 28 + PrivateHeaders/XCTest/XCTest.h | 34 + PrivateHeaders/XCTest/XCTestCase.h | 102 + PrivateHeaders/XCTest/XCTestCaseRun.h | 18 + PrivateHeaders/XCTest/XCTestCaseSuite.h | 19 + PrivateHeaders/XCTest/XCTestConfiguration.h | 66 + PrivateHeaders/XCTest/XCTestContext.h | 26 + PrivateHeaders/XCTest/XCTestContextScope.h | 19 + PrivateHeaders/XCTest/XCTestDriver.h | 38 + .../XCTest/XCTestDriverInterface-Protocol.h | 14 + PrivateHeaders/XCTest/XCTestExpectation.h | 39 + .../XCTestExpectationDelegate-Protocol.h | 14 + .../XCTest/XCTestExpectationWaiter.h | 17 + PrivateHeaders/XCTest/XCTestLog.h | 32 + .../XCTestManager_IDEInterface-Protocol.h | 36 + .../XCTestManager_ManagerInterface-Protocol.h | 54 + .../XCTestManager_TestsInterface-Protocol.h | 12 + PrivateHeaders/XCTest/XCTestMisuseObserver.h | 37 + .../XCTest/XCTestObservation-Protocol.h | 20 + .../XCTest/XCTestObservationCenter.h | 36 + PrivateHeaders/XCTest/XCTestObserver.h | 20 + PrivateHeaders/XCTest/XCTestProbe.h | 13 + PrivateHeaders/XCTest/XCTestRun.h | 39 + PrivateHeaders/XCTest/XCTestSuite.h | 48 + PrivateHeaders/XCTest/XCTestSuiteRun.h | 28 + PrivateHeaders/XCTest/XCTestWaiter.h | 14 + PrivateHeaders/XCTest/XCUIApplication.h | 60 + PrivateHeaders/XCTest/XCUIApplicationImpl.h | 38 + .../XCTest/XCUIApplicationProcess.h | 85 + PrivateHeaders/XCTest/XCUICoordinate.h | 44 + PrivateHeaders/XCTest/XCUIDevice.h | 30 + PrivateHeaders/XCTest/XCUIElement.h | 74 + .../XCUIElementAsynchronousHandlerWrapper.h | 19 + .../XCTest/XCUIElementHitPointCoordinate.h | 24 + PrivateHeaders/XCTest/XCUIElementQuery.h | 70 + PrivateHeaders/XCTest/XCUIHitPointResult.h | 20 + .../XCTest/XCUIRecorderNodeFinder.h | 65 + .../XCTest/XCUIRecorderNodeFinderMatch.h | 25 + .../XCTest/XCUIRecorderTimingMessage.h | 20 + PrivateHeaders/XCTest/XCUIRecorderUtilities.h | 45 + PrivateHeaders/XCTest/XCUIScreen.h | 27 + .../XCTest/XCUIScreenDataSource-Protocol.h | 19 + PrivateHeaders/XCTest/_XCInternalTestRun.h | 43 + .../XCTest/_XCKVOExpectationImplementation.h | 32 + ...winNotificationExpectationImplementation.h | 27 + ...TNSNotificationExpectationImplementation.h | 30 + ..._XCTNSPredicateExpectationImplementation.h | 29 + PrivateHeaders/XCTest/_XCTWaiterImpl.h | 41 + .../XCTest/_XCTestCaseImplementation.h | 70 + .../XCTest/_XCTestCaseInterruptionException.h | 13 + .../XCTest/_XCTestExpectationImplementation.h | 40 + PrivateHeaders/XCTest/_XCTestImplementation.h | 14 + .../_XCTestObservationCenterImplementation.h | 19 + .../XCTest/_XCTestSuiteImplementation.h | 23 + README.md | 56 + Scripts/build-webdriveragent.js | 79 + Scripts/build.sh | 104 + Scripts/ci/build-real.sh | 24 + Scripts/ci/build-sim.sh | 15 + Scripts/fetch-prebuilt-wda.js | 61 + Scripts/update-wda-version.js | 41 + WebDriverAgent.xcodeproj/project.pbxproj | 4551 +++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 143820 bytes .../xcschemes/IntegrationApp.xcscheme | 87 + .../xcschemes/IntegrationTests_1.xcscheme | 86 + .../xcschemes/IntegrationTests_2.xcscheme | 86 + .../xcschemes/IntegrationTests_3.xcscheme | 86 + .../xcschemes/WebDriverAgentLib.xcscheme | 125 + .../xcschemes/WebDriverAgentLib_tvOS.xcscheme | 95 + .../WebDriverAgentRunner-nodebug.xcscheme | 87 + .../xcschemes/WebDriverAgentRunner.xcscheme | 109 + .../WebDriverAgentRunner_tvOS.xcscheme | 118 + .../FBXCElementSnapshotWrapper+Helpers.h | 100 + .../FBXCElementSnapshotWrapper+Helpers.m | 192 + .../NSDictionary+FBUtf8SafeDictionary.h | 39 + .../NSDictionary+FBUtf8SafeDictionary.m | 87 + .../Categories/NSExpression+FBFormat.h | 29 + .../Categories/NSExpression+FBFormat.m | 34 + .../Categories/NSString+FBVisualLength.h | 18 + .../Categories/NSString+FBVisualLength.m | 18 + .../Categories/NSString+FBXMLSafeString.h | 27 + .../Categories/NSString+FBXMLSafeString.m | 30 + .../XCAXClient_iOS+FBSnapshotReqParams.h | 25 + .../XCAXClient_iOS+FBSnapshotReqParams.m | 82 + .../Categories/XCTIssue+FBPatcher.h | 17 + .../Categories/XCTIssue+FBPatcher.m | 35 + .../Categories/XCUIApplication+FBAlert.h | 23 + .../Categories/XCUIApplication+FBAlert.m | 109 + .../Categories/XCUIApplication+FBFocused.h | 23 + .../Categories/XCUIApplication+FBHelpers.h | 171 + .../Categories/XCUIApplication+FBHelpers.m | 644 ++ .../Categories/XCUIApplication+FBQuiescence.h | 24 + .../Categories/XCUIApplication+FBQuiescence.m | 28 + .../XCUIApplication+FBTouchAction.h | 29 + .../XCUIApplication+FBTouchAction.m | 75 + .../XCUIApplication+FBUIInterruptions.h | 22 + .../XCUIApplication+FBUIInterruptions.m | 31 + .../XCUIApplicationProcess+FBQuiescence.h | 27 + .../XCUIApplicationProcess+FBQuiescence.m | 118 + .../Categories/XCUIDevice+FBHealthCheck.h | 30 + .../Categories/XCUIDevice+FBHealthCheck.m | 47 + .../Categories/XCUIDevice+FBHelpers.h | 193 + .../Categories/XCUIDevice+FBHelpers.m | 388 + .../Categories/XCUIDevice+FBRotation.h | 38 + .../Categories/XCUIDevice+FBRotation.m | 68 + .../Categories/XCUIElement+FBAccessibility.h | 29 + .../Categories/XCUIElement+FBAccessibility.m | 50 + .../Categories/XCUIElement+FBCaching.h | 19 + .../Categories/XCUIElement+FBCaching.m | 36 + .../Categories/XCUIElement+FBClassChain.h | 57 + .../Categories/XCUIElement+FBClassChain.m | 98 + .../Categories/XCUIElement+FBFind.h | 75 + .../Categories/XCUIElement+FBFind.m | 133 + .../Categories/XCUIElement+FBForceTouch.h | 38 + .../Categories/XCUIElement+FBForceTouch.m | 52 + .../Categories/XCUIElement+FBIsVisible.h | 28 + .../Categories/XCUIElement+FBIsVisible.m | 78 + .../Categories/XCUIElement+FBMinMax.h | 33 + .../Categories/XCUIElement+FBMinMax.m | 75 + .../Categories/XCUIElement+FBPickerWheel.h | 45 + .../Categories/XCUIElement+FBPickerWheel.m | 58 + .../Categories/XCUIElement+FBResolve.h | 37 + .../Categories/XCUIElement+FBResolve.m | 51 + .../Categories/XCUIElement+FBScrolling.h | 98 + .../Categories/XCUIElement+FBScrolling.m | 342 + .../Categories/XCUIElement+FBSwiping.h | 38 + .../Categories/XCUIElement+FBSwiping.m | 56 + .../Categories/XCUIElement+FBTVFocuse.h | 35 + .../Categories/XCUIElement+FBTVFocuse.m | 71 + .../Categories/XCUIElement+FBTyping.h | 64 + .../Categories/XCUIElement+FBTyping.m | 199 + .../Categories/XCUIElement+FBUID.h | 42 + .../Categories/XCUIElement+FBUID.m | 85 + .../Categories/XCUIElement+FBUtilities.h | 103 + .../Categories/XCUIElement+FBUtilities.m | 170 + .../Categories/XCUIElement+FBVisibleFrame.h | 35 + .../Categories/XCUIElement+FBVisibleFrame.m | 52 + .../XCUIElement+FBWebDriverAttributes.h | 32 + .../XCUIElement+FBWebDriverAttributes.m | 283 + .../Categories/XCUIElementQuery+FBHelpers.h | 28 + .../Categories/XCUIElementQuery+FBHelpers.m | 46 + .../Commands/FBAlertViewCommands.h | 19 + .../Commands/FBAlertViewCommands.m | 130 + WebDriverAgentLib/Commands/FBCustomCommands.h | 19 + WebDriverAgentLib/Commands/FBCustomCommands.m | 633 ++ WebDriverAgentLib/Commands/FBDebugCommands.h | 19 + WebDriverAgentLib/Commands/FBDebugCommands.m | 81 + .../Commands/FBElementCommands.h | 19 + .../Commands/FBElementCommands.m | 819 ++ .../Commands/FBFindElementCommands.h | 18 + .../Commands/FBFindElementCommands.m | 186 + .../Commands/FBOrientationCommands.h | 19 + .../Commands/FBOrientationCommands.m | 185 + .../Commands/FBScreenshotCommands.h | 19 + .../Commands/FBScreenshotCommands.m | 40 + .../Commands/FBSessionCommands.h | 19 + .../Commands/FBSessionCommands.m | 586 ++ .../Commands/FBTouchActionCommands.h | 19 + .../Commands/FBTouchActionCommands.m | 41 + .../Commands/FBTouchIDCommands.h | 19 + .../Commands/FBTouchIDCommands.m | 30 + .../Commands/FBUnknownCommands.h | 19 + .../Commands/FBUnknownCommands.m | 39 + WebDriverAgentLib/Commands/FBVideoCommands.h | 19 + WebDriverAgentLib/Commands/FBVideoCommands.m | 84 + WebDriverAgentLib/FBAlert.h | 91 + WebDriverAgentLib/FBAlert.m | 274 + WebDriverAgentLib/Info.plist | 26 + WebDriverAgentLib/Routing/FBCommandHandler.h | 38 + WebDriverAgentLib/Routing/FBCommandStatus.h | 80 + WebDriverAgentLib/Routing/FBCommandStatus.m | 279 + WebDriverAgentLib/Routing/FBElement.h | 92 + WebDriverAgentLib/Routing/FBElementCache.h | 61 + WebDriverAgentLib/Routing/FBElementCache.m | 100 + WebDriverAgentLib/Routing/FBElementUtils.h | 76 + WebDriverAgentLib/Routing/FBElementUtils.m | 153 + .../Routing/FBExceptionHandler.h | 29 + .../Routing/FBExceptionHandler.m | 58 + WebDriverAgentLib/Routing/FBExceptions.h | 60 + WebDriverAgentLib/Routing/FBExceptions.m | 24 + WebDriverAgentLib/Routing/FBHTTPStatusCodes.h | 581 ++ .../Routing/FBResponseJSONPayload.h | 29 + .../Routing/FBResponseJSONPayload.m | 59 + WebDriverAgentLib/Routing/FBResponsePayload.h | 74 + WebDriverAgentLib/Routing/FBResponsePayload.m | 148 + WebDriverAgentLib/Routing/FBRoute.h | 77 + WebDriverAgentLib/Routing/FBRoute.m | 174 + .../Routing/FBRouteRequest-Private.h | 20 + WebDriverAgentLib/Routing/FBRouteRequest.h | 39 + WebDriverAgentLib/Routing/FBRouteRequest.m | 32 + .../Routing/FBScreenRecordingContainer.h | 56 + .../Routing/FBScreenRecordingContainer.m | 72 + .../Routing/FBScreenRecordingPromise.h | 29 + .../Routing/FBScreenRecordingPromise.m | 31 + .../Routing/FBScreenRecordingRequest.h | 39 + .../Routing/FBScreenRecordingRequest.m | 94 + WebDriverAgentLib/Routing/FBSession-Private.h | 26 + WebDriverAgentLib/Routing/FBSession.h | 141 + WebDriverAgentLib/Routing/FBSession.m | 295 + WebDriverAgentLib/Routing/FBTCPSocket.h | 65 + WebDriverAgentLib/Routing/FBTCPSocket.m | 85 + WebDriverAgentLib/Routing/FBWebServer.h | 52 + WebDriverAgentLib/Routing/FBWebServer.m | 240 + .../Routing/FBXCAccessibilityElement.h | 34 + .../Routing/FBXCAccessibilityElement.m | 17 + WebDriverAgentLib/Routing/FBXCDeviceEvent.h | 34 + WebDriverAgentLib/Routing/FBXCDeviceEvent.m | 42 + .../Routing/FBXCElementSnapshot.h | 82 + .../Routing/FBXCElementSnapshot.m | 9 + .../Routing/FBXCElementSnapshotWrapper.h | 29 + .../Routing/FBXCElementSnapshotWrapper.m | 114 + .../Utilities/FBAccessibilityTraits.h | 20 + .../Utilities/FBAccessibilityTraits.m | 61 + .../Utilities/FBActiveAppDetectionPoint.h | 56 + .../Utilities/FBActiveAppDetectionPoint.m | 87 + WebDriverAgentLib/Utilities/FBAlertsMonitor.h | 51 + WebDriverAgentLib/Utilities/FBAlertsMonitor.m | 94 + .../Utilities/FBBaseActionsSynthesizer.h | 131 + .../Utilities/FBBaseActionsSynthesizer.m | 165 + WebDriverAgentLib/Utilities/FBCapabilities.h | 45 + WebDriverAgentLib/Utilities/FBCapabilities.m | 25 + .../Utilities/FBClassChainQueryParser.h | 99 + .../Utilities/FBClassChainQueryParser.m | 666 ++ WebDriverAgentLib/Utilities/FBConfiguration.h | 375 + WebDriverAgentLib/Utilities/FBConfiguration.m | 677 ++ .../Utilities/FBDebugLogDelegateDecorator.h | 26 + .../Utilities/FBDebugLogDelegateDecorator.m | 49 + .../Utilities/FBElementHelpers.h | 29 + .../Utilities/FBElementHelpers.m | 21 + .../Utilities/FBElementTypeTransformer.h | 45 + .../Utilities/FBElementTypeTransformer.m | 149 + WebDriverAgentLib/Utilities/FBErrorBuilder.h | 64 + WebDriverAgentLib/Utilities/FBErrorBuilder.m | 75 + .../Utilities/FBFailureProofTestCase.h | 19 + .../Utilities/FBFailureProofTestCase.m | 68 + .../Utilities/FBImageProcessor.h | 55 + .../Utilities/FBImageProcessor.m | 170 + WebDriverAgentLib/Utilities/FBImageUtils.h | 25 + WebDriverAgentLib/Utilities/FBImageUtils.m | 80 + WebDriverAgentLib/Utilities/FBKeyboard.h | 37 + WebDriverAgentLib/Utilities/FBKeyboard.m | 123 + WebDriverAgentLib/Utilities/FBLogger.h | 32 + WebDriverAgentLib/Utilities/FBLogger.m | 47 + WebDriverAgentLib/Utilities/FBMacros.h | 57 + WebDriverAgentLib/Utilities/FBMathUtils.h | 36 + WebDriverAgentLib/Utilities/FBMathUtils.m | 63 + WebDriverAgentLib/Utilities/FBMjpegServer.h | 24 + WebDriverAgentLib/Utilities/FBMjpegServer.m | 151 + .../Utilities/FBNotificationsHelper.h | 37 + .../Utilities/FBNotificationsHelper.m | 29 + WebDriverAgentLib/Utilities/FBPasteboard.h | 40 + WebDriverAgentLib/Utilities/FBPasteboard.m | 172 + .../Utilities/FBProtocolHelpers.h | 46 + .../Utilities/FBProtocolHelpers.m | 144 + .../Utilities/FBReflectionUtils.h | 23 + .../Utilities/FBReflectionUtils.m | 31 + .../Utilities/FBRunLoopSpinner.h | 77 + .../Utilities/FBRunLoopSpinner.m | 94 + WebDriverAgentLib/Utilities/FBRuntimeUtils.h | 79 + WebDriverAgentLib/Utilities/FBRuntimeUtils.m | 112 + WebDriverAgentLib/Utilities/FBScreen.h | 22 + WebDriverAgentLib/Utilities/FBScreen.m | 21 + WebDriverAgentLib/Utilities/FBScreenshot.h | 44 + WebDriverAgentLib/Utilities/FBScreenshot.m | 227 + WebDriverAgentLib/Utilities/FBSettings.h | 46 + WebDriverAgentLib/Utilities/FBSettings.m | 40 + .../Utilities/FBTVNavigationTracker-Private.h | 27 + .../Utilities/FBTVNavigationTracker.h | 52 + .../Utilities/FBTVNavigationTracker.m | 142 + .../Utilities/FBUnattachedAppLauncher.h | 26 + .../Utilities/FBUnattachedAppLauncher.m | 21 + .../Utilities/FBW3CActionsHelpers.h | 42 + .../Utilities/FBW3CActionsHelpers.m | 119 + .../Utilities/FBW3CActionsSynthesizer.h | 19 + .../Utilities/FBW3CActionsSynthesizer.m | 885 ++ .../Utilities/FBWebServerParams.h | 22 + .../Utilities/FBWebServerParams.m | 23 + .../Utilities/FBXCAXClientProxy.h | 49 + .../Utilities/FBXCAXClientProxy.m | 119 + .../Utilities/FBXCTestDaemonsProxy.h | 45 + .../Utilities/FBXCTestDaemonsProxy.m | 359 + .../Utilities/FBXCodeCompatibility.h | 80 + .../Utilities/FBXCodeCompatibility.m | 99 + .../Utilities/FBXMLGenerationOptions.h | 45 + .../Utilities/FBXMLGenerationOptions.m | 25 + WebDriverAgentLib/Utilities/FBXPath-Private.h | 56 + WebDriverAgentLib/Utilities/FBXPath.h | 59 + WebDriverAgentLib/Utilities/FBXPath.m | 954 ++ .../Utilities/LRUCache/LRUCache.h | 66 + .../Utilities/LRUCache/LRUCache.m | 146 + .../Utilities/LRUCache/LRUCacheNode.h | 43 + .../Utilities/LRUCache/LRUCacheNode.m | 51 + .../Utilities/NSPredicate+FBFormat.h | 42 + .../Utilities/NSPredicate+FBFormat.m | 69 + .../Utilities/XCTestPrivateSymbols.h | 74 + .../Utilities/XCTestPrivateSymbols.m | 96 + .../Utilities/XCUIApplicationProcessDelay.h | 37 + .../Utilities/XCUIApplicationProcessDelay.m | 83 + .../Vendor/CocoaAsyncSocket/GCDAsyncSocket.h | 1220 +++ .../Vendor/CocoaAsyncSocket/GCDAsyncSocket.m | 8528 +++++++++++++++++ .../CocoaAsyncSocket/GCDAsyncUdpSocket.h | 1036 ++ .../CocoaAsyncSocket/GCDAsyncUdpSocket.m | 5642 +++++++++++ .../CocoaHTTPServer/Categories/DDNumber.h | 12 + .../CocoaHTTPServer/Categories/DDNumber.m | 88 + .../CocoaHTTPServer/Categories/DDRange.h | 56 + .../CocoaHTTPServer/Categories/DDRange.m | 106 + .../Vendor/CocoaHTTPServer/HTTPConnection.h | 107 + .../Vendor/CocoaHTTPServer/HTTPConnection.m | 2238 +++++ .../Vendor/CocoaHTTPServer/HTTPLogging.h | 122 + .../Vendor/CocoaHTTPServer/HTTPMessage.h | 48 + .../Vendor/CocoaHTTPServer/HTTPMessage.m | 114 + .../Vendor/CocoaHTTPServer/HTTPResponse.h | 149 + .../Vendor/CocoaHTTPServer/HTTPServer.h | 126 + .../Vendor/CocoaHTTPServer/HTTPServer.m | 372 + .../Vendor/CocoaHTTPServer/LICENSE | 18 + .../Responses/HTTPDataResponse.h | 13 + .../Responses/HTTPDataResponse.m | 83 + .../Responses/HTTPErrorResponse.h | 9 + .../Responses/HTTPErrorResponse.m | 40 + .../RoutingHTTPServer/HTTPResponseProxy.h | 13 + .../RoutingHTTPServer/HTTPResponseProxy.m | 84 + .../Vendor/RoutingHTTPServer/LICENSE | 19 + .../Vendor/RoutingHTTPServer/Route.h | 18 + .../Vendor/RoutingHTTPServer/Route.m | 11 + .../Vendor/RoutingHTTPServer/RouteRequest.h | 16 + .../Vendor/RoutingHTTPServer/RouteRequest.m | 50 + .../Vendor/RoutingHTTPServer/RouteResponse.h | 20 + .../Vendor/RoutingHTTPServer/RouteResponse.m | 66 + .../RoutingHTTPServer/RoutingConnection.h | 5 + .../RoutingHTTPServer/RoutingConnection.m | 142 + .../RoutingHTTPServer/RoutingHTTPServer.h | 55 + .../RoutingHTTPServer/RoutingHTTPServer.m | 303 + WebDriverAgentLib/WebDriverAgentLib.h | 59 + WebDriverAgentRunner/Info.plist | 35 + WebDriverAgentRunner/UITestingUITests.m | 58 + .../IntegrationApp/Classes/AppDelegate.h | 15 + .../IntegrationApp/Classes/AppDelegate.m | 15 + .../Classes/FBAlertViewController.h | 14 + .../Classes/FBAlertViewController.m | 82 + .../Classes/FBNavigationController.h | 12 + .../Classes/FBNavigationController.m | 25 + .../Classes/FBScrollViewController.h | 13 + .../Classes/FBScrollViewController.m | 39 + .../Classes/FBTableDataSource.h | 16 + .../Classes/FBTableDataSource.m | 35 + .../IntegrationApp/Classes/TouchSpotView.h | 17 + .../IntegrationApp/Classes/TouchSpotView.m | 28 + .../Classes/TouchViewController.h | 23 + .../Classes/TouchViewController.m | 29 + .../IntegrationApp/Classes/TouchableView.h | 29 + .../IntegrationApp/Classes/TouchableView.m | 108 + .../IntegrationApp/Classes/ViewController.h | 12 + .../IntegrationApp/Classes/ViewController.m | 66 + WebDriverAgentTests/IntegrationApp/Info.plist | 56 + .../Resources/Base.lproj/Main.storyboard | 617 ++ WebDriverAgentTests/IntegrationApp/main.m | 15 + .../IntegrationTests/FBAlertTests.m | 188 + .../FBAutoAlertsHandlerTests.m | 73 + .../IntegrationTests/FBConfigurationTests.m | 38 + .../FBElementAttributeTests.m | 250 + .../IntegrationTests/FBElementSwipingTests.m | 127 + .../FBElementVisibilityTests.m | 73 + .../FBFailureProofTestCaseTests.m | 41 + .../IntegrationTests/FBForceTouchTests.m | 85 + .../IntegrationTests/FBImageProcessorTests.m | 82 + .../IntegrationTests/FBIntegrationTestCase.h | 76 + .../IntegrationTests/FBIntegrationTestCase.m | 140 + .../IntegrationTests/FBKeyboardTests.m | 61 + .../IntegrationTests/FBPasteboardTests.m | 91 + .../FBPickerWheelSelectTests.m | 55 + .../IntegrationTests/FBSafariAlertTests.m | 73 + .../IntegrationTests/FBScreenTests.m | 31 + .../IntegrationTests/FBScrollingTests.m | 126 + .../FBSessionIntegrationTests.m | 133 + .../IntegrationTests/FBTapTest.m | 103 + .../IntegrationTests/FBTestMacros.h | 33 + .../IntegrationTests/FBTypingTest.m | 68 + .../IntegrationTests/FBVideoRecordingTests.m | 64 + .../FBW3CMultiTouchActionsIntegrationTests.m | 124 + .../FBW3CTouchActionsIntegrationTests.m | 491 + .../IntegrationTests/FBW3CTypeActionsTests.m | 184 + .../FBXPathIntegrationTests.m | 157 + .../IntegrationTests/Info.plist | 24 + .../XCElementSnapshotHelperTests.m | 209 + .../XCElementSnapshotHitPointTests.m | 30 + .../XCUIApplicationHelperTests.m | 163 + .../XCUIDeviceHealthCheckTests.m | 27 + .../IntegrationTests/XCUIDeviceHelperTests.m | 220 + .../XCUIDeviceRotationTests.m | 84 + .../XCUIElementAttributesTests.m | 248 + .../IntegrationTests/XCUIElementFBFindTests.m | 472 + .../XCUIElementHelperIntegrationTests.m | 51 + .../Doubles/XCElementSnapshotDouble.h | 15 + .../Doubles/XCElementSnapshotDouble.m | 114 + .../UnitTests/Doubles/XCUIApplicationDouble.h | 17 + .../UnitTests/Doubles/XCUIApplicationDouble.m | 66 + .../UnitTests/Doubles/XCUIElementDouble.h | 50 + .../UnitTests/Doubles/XCUIElementDouble.m | 103 + .../UnitTests/FBClassChainTests.m | 313 + .../UnitTests/FBConfigurationTests.m | 48 + .../UnitTests/FBElementCacheTests.m | 124 + .../UnitTests/FBElementTypeTransformerTests.m | 45 + .../UnitTests/FBElementUtilitiesTests.m | 36 + .../UnitTests/FBErrorBuilderTests.m | 61 + .../UnitTests/FBExceptionHandlerTests.m | 59 + .../UnitTests/FBLRUCacheTests.m | 127 + .../UnitTests/FBMathUtilsTests.m | 113 + .../UnitTests/FBProtocolHelpersTests.m | 81 + WebDriverAgentTests/UnitTests/FBRouteTests.m | 129 + .../UnitTests/FBRunLoopSpinnerTests.m | 98 + .../UnitTests/FBRuntimeUtilsTests.m | 45 + .../UnitTests/FBSDKVersionTests.m | 66 + .../UnitTests/FBSessionTests.m | 67 + .../UnitTests/FBXMLSafeStringTests.m | 35 + WebDriverAgentTests/UnitTests/FBXPathTests.m | 152 + WebDriverAgentTests/UnitTests/Info.plist | 24 + .../UnitTests/NSDictionaryFBUtf8SafeTests.m | 34 + .../UnitTests/NSExpressionFBFormatTests.m | 60 + .../UnitTests/NSPredicateFBFormatTests.m | 72 + .../UnitTests/XCUIElementHelpersTests.m | 50 + .../Doubles/XCUIElementDouble.h | 45 + .../Doubles/XCUIElementDouble.m | 79 + .../FBTVNavigationTrackerTests.m | 86 + WebDriverAgentTests/UnitTests_tvOS/Info.plist | 24 + azure-templates/base_job.yml | 36 + azure-templates/bootstrap_steps.yml | 7 + azure-templates/node_setup_steps.yml | 4 + ci-jobs/scripts/azure-print-tag-name.js | 3 + ci-jobs/scripts/build-webdriveragents.js | 57 + ci-jobs/templates/build.yml | 38 + eslint.config.mjs | 13 + index.ts | 7 + lib/check-dependencies.js | 50 + lib/constants.js | 24 + lib/logger.js | 5 + lib/no-session-proxy.js | 26 + lib/types.ts | 52 + lib/utils.js | 398 + lib/webdriveragent.js | 741 ++ lib/xcodebuild.js | 466 + package.json | 105 + test/functional/desired.js | 5 + test/functional/helpers/simulator.js | 41 + test/functional/webdriveragent-e2e-specs.js | 129 + test/unit/utils-specs.js | 172 + test/unit/webdriveragent-specs.js | 412 + tsconfig.json | 15 + 512 files changed, 65167 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Configurations/IOSSettings.xcconfig create mode 100644 Configurations/IOSTestSettings.xcconfig create mode 100644 Configurations/TVOSSettings.xcconfig create mode 100644 Configurations/TVOSTestSettings.xcconfig create mode 100644 Fastlane/Fastfile create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 PrivateHeaders/AccessibilityUtilities/AXSettings.h create mode 100644 PrivateHeaders/MobileCoreServices/LSApplicationWorkspace.h create mode 100644 PrivateHeaders/TextInput/TIPreferencesController.h create mode 100644 PrivateHeaders/UIKitCore/UIKeyboardImpl.h create mode 100644 PrivateHeaders/XCTest/CDStructures.h create mode 100644 PrivateHeaders/XCTest/NSString-XCTAdditions.h create mode 100644 PrivateHeaders/XCTest/NSValue-XCTestAdditions.h create mode 100644 PrivateHeaders/XCTest/UIGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/UILongPressGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/UIPanGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/UIPinchGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/UISwipeGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/UITapGestureRecognizer-RecordingAdditions.h create mode 100644 PrivateHeaders/XCTest/XCAXClient_iOS.h create mode 100644 PrivateHeaders/XCTest/XCActivityRecord.h create mode 100644 PrivateHeaders/XCTest/XCApplicationMonitor.h create mode 100644 PrivateHeaders/XCTest/XCApplicationMonitor_iOS.h create mode 100644 PrivateHeaders/XCTest/XCApplicationQuery.h create mode 100644 PrivateHeaders/XCTest/XCDebugLogDelegate-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCDeviceEvent.h create mode 100644 PrivateHeaders/XCTest/XCEventGenerator.h create mode 100644 PrivateHeaders/XCTest/XCKeyMappingPath.h create mode 100644 PrivateHeaders/XCTest/XCKeyboardInputSolver.h create mode 100644 PrivateHeaders/XCTest/XCKeyboardKeyMap.h create mode 100644 PrivateHeaders/XCTest/XCKeyboardLayout.h create mode 100644 PrivateHeaders/XCTest/XCPointerEvent.h create mode 100644 PrivateHeaders/XCTest/XCPointerEventPath.h create mode 100644 PrivateHeaders/XCTest/XCSourceCodeRecording.h create mode 100644 PrivateHeaders/XCTest/XCSourceCodeTreeNode.h create mode 100644 PrivateHeaders/XCTest/XCSourceCodeTreeNodeEnumerator.h create mode 100644 PrivateHeaders/XCTest/XCSymbolicationRecord.h create mode 100644 PrivateHeaders/XCTest/XCSymbolicatorHolder.h create mode 100644 PrivateHeaders/XCTest/XCSynthesizedEventRecord.h create mode 100644 PrivateHeaders/XCTest/XCTAXClient-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTAsyncActivity-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTAsyncActivity.h create mode 100644 PrivateHeaders/XCTest/XCTAutomationTarget-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTDarwinNotificationExpectation.h create mode 100644 PrivateHeaders/XCTest/XCTElementSetTransformer-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTKVOExpectation.h create mode 100644 PrivateHeaders/XCTest/XCTMetric.h create mode 100644 PrivateHeaders/XCTest/XCTNSNotificationExpectation.h create mode 100644 PrivateHeaders/XCTest/XCTNSPredicateExpectation.h create mode 100644 PrivateHeaders/XCTest/XCTNSPredicateExpectationObject-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTRunnerAutomationSession.h create mode 100644 PrivateHeaders/XCTest/XCTRunnerDaemonSession.h create mode 100644 PrivateHeaders/XCTest/XCTRunnerIDESession.h create mode 100644 PrivateHeaders/XCTest/XCTTestRunSession.h create mode 100644 PrivateHeaders/XCTest/XCTTestRunSessionDelegate-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTUIApplicationMonitor-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTWaiter.h create mode 100644 PrivateHeaders/XCTest/XCTWaiterDelegate-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTWaiterDelegatePrivate-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTWaiterManagement-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTWaiterManager.h create mode 100644 PrivateHeaders/XCTest/XCTest.h create mode 100644 PrivateHeaders/XCTest/XCTestCase.h create mode 100644 PrivateHeaders/XCTest/XCTestCaseRun.h create mode 100644 PrivateHeaders/XCTest/XCTestCaseSuite.h create mode 100644 PrivateHeaders/XCTest/XCTestConfiguration.h create mode 100644 PrivateHeaders/XCTest/XCTestContext.h create mode 100644 PrivateHeaders/XCTest/XCTestContextScope.h create mode 100644 PrivateHeaders/XCTest/XCTestDriver.h create mode 100644 PrivateHeaders/XCTest/XCTestDriverInterface-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestExpectation.h create mode 100644 PrivateHeaders/XCTest/XCTestExpectationDelegate-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestExpectationWaiter.h create mode 100644 PrivateHeaders/XCTest/XCTestLog.h create mode 100644 PrivateHeaders/XCTest/XCTestManager_IDEInterface-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestManager_TestsInterface-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestMisuseObserver.h create mode 100644 PrivateHeaders/XCTest/XCTestObservation-Protocol.h create mode 100644 PrivateHeaders/XCTest/XCTestObservationCenter.h create mode 100644 PrivateHeaders/XCTest/XCTestObserver.h create mode 100644 PrivateHeaders/XCTest/XCTestProbe.h create mode 100644 PrivateHeaders/XCTest/XCTestRun.h create mode 100644 PrivateHeaders/XCTest/XCTestSuite.h create mode 100644 PrivateHeaders/XCTest/XCTestSuiteRun.h create mode 100644 PrivateHeaders/XCTest/XCTestWaiter.h create mode 100644 PrivateHeaders/XCTest/XCUIApplication.h create mode 100644 PrivateHeaders/XCTest/XCUIApplicationImpl.h create mode 100644 PrivateHeaders/XCTest/XCUIApplicationProcess.h create mode 100644 PrivateHeaders/XCTest/XCUICoordinate.h create mode 100644 PrivateHeaders/XCTest/XCUIDevice.h create mode 100644 PrivateHeaders/XCTest/XCUIElement.h create mode 100644 PrivateHeaders/XCTest/XCUIElementAsynchronousHandlerWrapper.h create mode 100644 PrivateHeaders/XCTest/XCUIElementHitPointCoordinate.h create mode 100644 PrivateHeaders/XCTest/XCUIElementQuery.h create mode 100644 PrivateHeaders/XCTest/XCUIHitPointResult.h create mode 100644 PrivateHeaders/XCTest/XCUIRecorderNodeFinder.h create mode 100644 PrivateHeaders/XCTest/XCUIRecorderNodeFinderMatch.h create mode 100644 PrivateHeaders/XCTest/XCUIRecorderTimingMessage.h create mode 100644 PrivateHeaders/XCTest/XCUIRecorderUtilities.h create mode 100644 PrivateHeaders/XCTest/XCUIScreen.h create mode 100644 PrivateHeaders/XCTest/XCUIScreenDataSource-Protocol.h create mode 100644 PrivateHeaders/XCTest/_XCInternalTestRun.h create mode 100644 PrivateHeaders/XCTest/_XCKVOExpectationImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTDarwinNotificationExpectationImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTNSNotificationExpectationImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTNSPredicateExpectationImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTWaiterImpl.h create mode 100644 PrivateHeaders/XCTest/_XCTestCaseImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTestCaseInterruptionException.h create mode 100644 PrivateHeaders/XCTest/_XCTestExpectationImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTestImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTestObservationCenterImplementation.h create mode 100644 PrivateHeaders/XCTest/_XCTestSuiteImplementation.h create mode 100644 README.md create mode 100644 Scripts/build-webdriveragent.js create mode 100755 Scripts/build.sh create mode 100755 Scripts/ci/build-real.sh create mode 100755 Scripts/ci/build-sim.sh create mode 100644 Scripts/fetch-prebuilt-wda.js create mode 100644 Scripts/update-wda-version.js create mode 100644 WebDriverAgent.xcodeproj/project.pbxproj create mode 100644 WebDriverAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 WebDriverAgent.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 WebDriverAgent.xcodeproj/project.xcworkspace/xcuserdata/zhangwei.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_1.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_2.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_3.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_tvOS.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner-nodebug.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme create mode 100644 WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme create mode 100644 WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.h create mode 100644 WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.m create mode 100644 WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h create mode 100644 WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m create mode 100644 WebDriverAgentLib/Categories/NSExpression+FBFormat.h create mode 100644 WebDriverAgentLib/Categories/NSExpression+FBFormat.m create mode 100644 WebDriverAgentLib/Categories/NSString+FBVisualLength.h create mode 100644 WebDriverAgentLib/Categories/NSString+FBVisualLength.m create mode 100644 WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h create mode 100644 WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m create mode 100644 WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h create mode 100644 WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m create mode 100644 WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h create mode 100644 WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m create mode 100644 WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h create mode 100644 WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBCaching.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBCaching.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBFind.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBFind.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBResolve.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBResolve.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBTyping.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBTyping.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBUID.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBUID.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h create mode 100644 WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m create mode 100644 WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h create mode 100644 WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m create mode 100644 WebDriverAgentLib/Commands/FBAlertViewCommands.h create mode 100644 WebDriverAgentLib/Commands/FBAlertViewCommands.m create mode 100644 WebDriverAgentLib/Commands/FBCustomCommands.h create mode 100644 WebDriverAgentLib/Commands/FBCustomCommands.m create mode 100644 WebDriverAgentLib/Commands/FBDebugCommands.h create mode 100644 WebDriverAgentLib/Commands/FBDebugCommands.m create mode 100644 WebDriverAgentLib/Commands/FBElementCommands.h create mode 100644 WebDriverAgentLib/Commands/FBElementCommands.m create mode 100644 WebDriverAgentLib/Commands/FBFindElementCommands.h create mode 100644 WebDriverAgentLib/Commands/FBFindElementCommands.m create mode 100644 WebDriverAgentLib/Commands/FBOrientationCommands.h create mode 100644 WebDriverAgentLib/Commands/FBOrientationCommands.m create mode 100644 WebDriverAgentLib/Commands/FBScreenshotCommands.h create mode 100644 WebDriverAgentLib/Commands/FBScreenshotCommands.m create mode 100644 WebDriverAgentLib/Commands/FBSessionCommands.h create mode 100644 WebDriverAgentLib/Commands/FBSessionCommands.m create mode 100644 WebDriverAgentLib/Commands/FBTouchActionCommands.h create mode 100644 WebDriverAgentLib/Commands/FBTouchActionCommands.m create mode 100644 WebDriverAgentLib/Commands/FBTouchIDCommands.h create mode 100644 WebDriverAgentLib/Commands/FBTouchIDCommands.m create mode 100644 WebDriverAgentLib/Commands/FBUnknownCommands.h create mode 100644 WebDriverAgentLib/Commands/FBUnknownCommands.m create mode 100644 WebDriverAgentLib/Commands/FBVideoCommands.h create mode 100644 WebDriverAgentLib/Commands/FBVideoCommands.m create mode 100644 WebDriverAgentLib/FBAlert.h create mode 100644 WebDriverAgentLib/FBAlert.m create mode 100644 WebDriverAgentLib/Info.plist create mode 100644 WebDriverAgentLib/Routing/FBCommandHandler.h create mode 100644 WebDriverAgentLib/Routing/FBCommandStatus.h create mode 100644 WebDriverAgentLib/Routing/FBCommandStatus.m create mode 100644 WebDriverAgentLib/Routing/FBElement.h create mode 100644 WebDriverAgentLib/Routing/FBElementCache.h create mode 100644 WebDriverAgentLib/Routing/FBElementCache.m create mode 100644 WebDriverAgentLib/Routing/FBElementUtils.h create mode 100644 WebDriverAgentLib/Routing/FBElementUtils.m create mode 100644 WebDriverAgentLib/Routing/FBExceptionHandler.h create mode 100644 WebDriverAgentLib/Routing/FBExceptionHandler.m create mode 100644 WebDriverAgentLib/Routing/FBExceptions.h create mode 100644 WebDriverAgentLib/Routing/FBExceptions.m create mode 100644 WebDriverAgentLib/Routing/FBHTTPStatusCodes.h create mode 100644 WebDriverAgentLib/Routing/FBResponseJSONPayload.h create mode 100644 WebDriverAgentLib/Routing/FBResponseJSONPayload.m create mode 100644 WebDriverAgentLib/Routing/FBResponsePayload.h create mode 100644 WebDriverAgentLib/Routing/FBResponsePayload.m create mode 100644 WebDriverAgentLib/Routing/FBRoute.h create mode 100644 WebDriverAgentLib/Routing/FBRoute.m create mode 100644 WebDriverAgentLib/Routing/FBRouteRequest-Private.h create mode 100644 WebDriverAgentLib/Routing/FBRouteRequest.h create mode 100644 WebDriverAgentLib/Routing/FBRouteRequest.m create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingContainer.h create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingContainer.m create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingPromise.h create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingPromise.m create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingRequest.h create mode 100644 WebDriverAgentLib/Routing/FBScreenRecordingRequest.m create mode 100644 WebDriverAgentLib/Routing/FBSession-Private.h create mode 100644 WebDriverAgentLib/Routing/FBSession.h create mode 100644 WebDriverAgentLib/Routing/FBSession.m create mode 100644 WebDriverAgentLib/Routing/FBTCPSocket.h create mode 100644 WebDriverAgentLib/Routing/FBTCPSocket.m create mode 100644 WebDriverAgentLib/Routing/FBWebServer.h create mode 100644 WebDriverAgentLib/Routing/FBWebServer.m create mode 100644 WebDriverAgentLib/Routing/FBXCAccessibilityElement.h create mode 100644 WebDriverAgentLib/Routing/FBXCAccessibilityElement.m create mode 100644 WebDriverAgentLib/Routing/FBXCDeviceEvent.h create mode 100644 WebDriverAgentLib/Routing/FBXCDeviceEvent.m create mode 100644 WebDriverAgentLib/Routing/FBXCElementSnapshot.h create mode 100644 WebDriverAgentLib/Routing/FBXCElementSnapshot.m create mode 100644 WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h create mode 100644 WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m create mode 100644 WebDriverAgentLib/Utilities/FBAccessibilityTraits.h create mode 100644 WebDriverAgentLib/Utilities/FBAccessibilityTraits.m create mode 100644 WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h create mode 100644 WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m create mode 100644 WebDriverAgentLib/Utilities/FBAlertsMonitor.h create mode 100644 WebDriverAgentLib/Utilities/FBAlertsMonitor.m create mode 100644 WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h create mode 100644 WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m create mode 100644 WebDriverAgentLib/Utilities/FBCapabilities.h create mode 100644 WebDriverAgentLib/Utilities/FBCapabilities.m create mode 100644 WebDriverAgentLib/Utilities/FBClassChainQueryParser.h create mode 100644 WebDriverAgentLib/Utilities/FBClassChainQueryParser.m create mode 100644 WebDriverAgentLib/Utilities/FBConfiguration.h create mode 100644 WebDriverAgentLib/Utilities/FBConfiguration.m create mode 100644 WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h create mode 100644 WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m create mode 100644 WebDriverAgentLib/Utilities/FBElementHelpers.h create mode 100644 WebDriverAgentLib/Utilities/FBElementHelpers.m create mode 100644 WebDriverAgentLib/Utilities/FBElementTypeTransformer.h create mode 100644 WebDriverAgentLib/Utilities/FBElementTypeTransformer.m create mode 100644 WebDriverAgentLib/Utilities/FBErrorBuilder.h create mode 100644 WebDriverAgentLib/Utilities/FBErrorBuilder.m create mode 100644 WebDriverAgentLib/Utilities/FBFailureProofTestCase.h create mode 100644 WebDriverAgentLib/Utilities/FBFailureProofTestCase.m create mode 100644 WebDriverAgentLib/Utilities/FBImageProcessor.h create mode 100644 WebDriverAgentLib/Utilities/FBImageProcessor.m create mode 100644 WebDriverAgentLib/Utilities/FBImageUtils.h create mode 100644 WebDriverAgentLib/Utilities/FBImageUtils.m create mode 100644 WebDriverAgentLib/Utilities/FBKeyboard.h create mode 100644 WebDriverAgentLib/Utilities/FBKeyboard.m create mode 100644 WebDriverAgentLib/Utilities/FBLogger.h create mode 100644 WebDriverAgentLib/Utilities/FBLogger.m create mode 100644 WebDriverAgentLib/Utilities/FBMacros.h create mode 100644 WebDriverAgentLib/Utilities/FBMathUtils.h create mode 100644 WebDriverAgentLib/Utilities/FBMathUtils.m create mode 100644 WebDriverAgentLib/Utilities/FBMjpegServer.h create mode 100644 WebDriverAgentLib/Utilities/FBMjpegServer.m create mode 100644 WebDriverAgentLib/Utilities/FBNotificationsHelper.h create mode 100644 WebDriverAgentLib/Utilities/FBNotificationsHelper.m create mode 100644 WebDriverAgentLib/Utilities/FBPasteboard.h create mode 100644 WebDriverAgentLib/Utilities/FBPasteboard.m create mode 100644 WebDriverAgentLib/Utilities/FBProtocolHelpers.h create mode 100644 WebDriverAgentLib/Utilities/FBProtocolHelpers.m create mode 100644 WebDriverAgentLib/Utilities/FBReflectionUtils.h create mode 100644 WebDriverAgentLib/Utilities/FBReflectionUtils.m create mode 100644 WebDriverAgentLib/Utilities/FBRunLoopSpinner.h create mode 100644 WebDriverAgentLib/Utilities/FBRunLoopSpinner.m create mode 100644 WebDriverAgentLib/Utilities/FBRuntimeUtils.h create mode 100644 WebDriverAgentLib/Utilities/FBRuntimeUtils.m create mode 100644 WebDriverAgentLib/Utilities/FBScreen.h create mode 100644 WebDriverAgentLib/Utilities/FBScreen.m create mode 100644 WebDriverAgentLib/Utilities/FBScreenshot.h create mode 100644 WebDriverAgentLib/Utilities/FBScreenshot.m create mode 100644 WebDriverAgentLib/Utilities/FBSettings.h create mode 100644 WebDriverAgentLib/Utilities/FBSettings.m create mode 100644 WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h create mode 100644 WebDriverAgentLib/Utilities/FBTVNavigationTracker.h create mode 100644 WebDriverAgentLib/Utilities/FBTVNavigationTracker.m create mode 100644 WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h create mode 100644 WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m create mode 100644 WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h create mode 100644 WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m create mode 100644 WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h create mode 100644 WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m create mode 100644 WebDriverAgentLib/Utilities/FBWebServerParams.h create mode 100644 WebDriverAgentLib/Utilities/FBWebServerParams.m create mode 100644 WebDriverAgentLib/Utilities/FBXCAXClientProxy.h create mode 100644 WebDriverAgentLib/Utilities/FBXCAXClientProxy.m create mode 100644 WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h create mode 100644 WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m create mode 100644 WebDriverAgentLib/Utilities/FBXCodeCompatibility.h create mode 100644 WebDriverAgentLib/Utilities/FBXCodeCompatibility.m create mode 100644 WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h create mode 100644 WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m create mode 100644 WebDriverAgentLib/Utilities/FBXPath-Private.h create mode 100644 WebDriverAgentLib/Utilities/FBXPath.h create mode 100644 WebDriverAgentLib/Utilities/FBXPath.m create mode 100644 WebDriverAgentLib/Utilities/LRUCache/LRUCache.h create mode 100644 WebDriverAgentLib/Utilities/LRUCache/LRUCache.m create mode 100644 WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h create mode 100644 WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m create mode 100644 WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h create mode 100644 WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m create mode 100644 WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h create mode 100644 WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m create mode 100644 WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h create mode 100644 WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m create mode 100644 WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h create mode 100755 WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m create mode 100644 WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h create mode 100755 WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h create mode 100644 WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h create mode 100644 WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m create mode 100644 WebDriverAgentLib/WebDriverAgentLib.h create mode 100644 WebDriverAgentRunner/Info.plist create mode 100644 WebDriverAgentRunner/UITestingUITests.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/ViewController.h create mode 100644 WebDriverAgentTests/IntegrationApp/Classes/ViewController.m create mode 100644 WebDriverAgentTests/IntegrationApp/Info.plist create mode 100644 WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard create mode 100644 WebDriverAgentTests/IntegrationApp/main.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBAlertTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h create mode 100644 WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBScreenTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBScrollingTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBTapTest.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBTestMacros.h create mode 100644 WebDriverAgentTests/IntegrationTests/FBTypingTest.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/Info.plist create mode 100644 WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m create mode 100644 WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h create mode 100644 WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m create mode 100644 WebDriverAgentTests/UnitTests/FBClassChainTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBConfigurationTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBElementCacheTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBLRUCacheTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBMathUtilsTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBRouteTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBSDKVersionTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBSessionTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m create mode 100644 WebDriverAgentTests/UnitTests/FBXPathTests.m create mode 100644 WebDriverAgentTests/UnitTests/Info.plist create mode 100644 WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m create mode 100644 WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m create mode 100644 WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m create mode 100644 WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m create mode 100644 WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h create mode 100644 WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m create mode 100644 WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m create mode 100644 WebDriverAgentTests/UnitTests_tvOS/Info.plist create mode 100644 azure-templates/base_job.yml create mode 100644 azure-templates/bootstrap_steps.yml create mode 100644 azure-templates/node_setup_steps.yml create mode 100644 ci-jobs/scripts/azure-print-tag-name.js create mode 100644 ci-jobs/scripts/build-webdriveragents.js create mode 100644 ci-jobs/templates/build.yml create mode 100644 eslint.config.mjs create mode 100644 index.ts create mode 100644 lib/check-dependencies.js create mode 100644 lib/constants.js create mode 100644 lib/logger.js create mode 100644 lib/no-session-proxy.js create mode 100644 lib/types.ts create mode 100644 lib/utils.js create mode 100644 lib/webdriveragent.js create mode 100644 lib/xcodebuild.js create mode 100644 package.json create mode 100644 test/functional/desired.js create mode 100644 test/functional/helpers/simulator.js create mode 100644 test/functional/webdriveragent-e2e-specs.js create mode 100644 test/unit/utils-specs.js create mode 100644 test/unit/webdriveragent-specs.js create mode 100644 tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..173a006 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1360 @@ +## [10.1.2](https://github.com/appium/WebDriverAgent/compare/v10.1.1...v10.1.2) (2025-10-08) + +### Miscellaneous Chores + +* Skip staleness checks for subelement lookups ([#1063](https://github.com/appium/WebDriverAgent/issues/1063)) ([ada7760](https://github.com/appium/WebDriverAgent/commit/ada77604f9fa9bfc85c61cabbd2a9f4de00aceb9)) + +## [10.1.1](https://github.com/appium/WebDriverAgent/compare/v10.1.0...v10.1.1) (2025-09-12) + +### Miscellaneous Chores + +* remove patents file ([#1061](https://github.com/appium/WebDriverAgent/issues/1061)) ([b001c4e](https://github.com/appium/WebDriverAgent/commit/b001c4e39ef71cb8b91ef7391b418f32a7ebe21c)) + +## [10.1.0](https://github.com/appium/WebDriverAgent/compare/v10.0.1...v10.1.0) (2025-09-03) + +### Features + +* Add process and bundle identifiers to the application node in the XML source ([#1055](https://github.com/appium/WebDriverAgent/issues/1055)) ([088cff2](https://github.com/appium/WebDriverAgent/commit/088cff2b2bc19ddde698ec06f1db37c6989cf392)) + +## [10.0.1](https://github.com/appium/WebDriverAgent/compare/v10.0.0...v10.0.1) (2025-08-23) + +### Miscellaneous Chores + +* **deps-dev:** bump chai from 5.3.2 to 6.0.0 ([#1053](https://github.com/appium/WebDriverAgent/issues/1053)) ([9e9ec38](https://github.com/appium/WebDriverAgent/commit/9e9ec381bd6695e1c8b89f2a9c304b12385c0134)) + +## [10.0.0](https://github.com/appium/WebDriverAgent/compare/v9.15.3...v10.0.0) (2025-08-17) + +### ⚠ BREAKING CHANGES + +* Required Node.js version has been bumped to ^20.19.0 || ^22.12.0 || >=24.0.0 +* Required npm version has been bumped to >=10 +* Required base driver version has been bumped to >=10.0.0-rc.1 + +### Features + +* Update server compatibility ([#1051](https://github.com/appium/WebDriverAgent/issues/1051)) ([f9ea1e5](https://github.com/appium/WebDriverAgent/commit/f9ea1e5e2f5306030387d5293f073b2a6fe658e7)) + +## [9.15.3](https://github.com/appium/WebDriverAgent/compare/v9.15.2...v9.15.3) (2025-08-12) + +### Miscellaneous Chores + +* Cache application instances for their PIDs ([#1049](https://github.com/appium/WebDriverAgent/issues/1049)) ([e9cbf64](https://github.com/appium/WebDriverAgent/commit/e9cbf640c21243c304b476a497f33802e0501a7d)) + +## [9.15.2](https://github.com/appium/WebDriverAgent/compare/v9.15.1...v9.15.2) (2025-08-04) + +### Miscellaneous Chores + +* bump appium-ios-device to 2.9.0 ([#1047](https://github.com/appium/WebDriverAgent/issues/1047)) ([305019d](https://github.com/appium/WebDriverAgent/commit/305019d4dde89853e44c58170e17ec23c89de2f3)) + +## [9.15.1](https://github.com/appium/WebDriverAgent/compare/v9.15.0...v9.15.1) (2025-07-17) + +### Miscellaneous Chores + +* Remove the redundant check after activating the system app ([#1043](https://github.com/appium/WebDriverAgent/issues/1043)) ([33ccba1](https://github.com/appium/WebDriverAgent/commit/33ccba1ab3bc2980349f8553fd30aa5b08141b6b)) + +## [9.15.0](https://github.com/appium/WebDriverAgent/compare/v9.14.6...v9.15.0) (2025-07-10) + +### Features + +* HTTPS support for wda-client if webDriverAgentUrl is set ([#1042](https://github.com/appium/WebDriverAgent/issues/1042)) ([f7c4193](https://github.com/appium/WebDriverAgent/commit/f7c41939c793cdbc62e9c14d8eb91e06957bb566)) + +## [9.14.6](https://github.com/appium/WebDriverAgent/compare/v9.14.5...v9.14.6) (2025-06-24) + +### Miscellaneous Chores + +* add missing arch ([#1039](https://github.com/appium/WebDriverAgent/issues/1039)) ([a8dd958](https://github.com/appium/WebDriverAgent/commit/a8dd958bd92ef685bc1798ec04e92080b798d7d2)) + +## [9.14.5](https://github.com/appium/WebDriverAgent/compare/v9.14.4...v9.14.5) (2025-06-24) + +### Miscellaneous Chores + +* keep entire app for simulators ([d2bbcc6](https://github.com/appium/WebDriverAgent/commit/d2bbcc6d7af6b8eea076e24cd18429b74eeaffd6)) + +## [9.14.4](https://github.com/appium/WebDriverAgent/compare/v9.14.3...v9.14.4) (2025-06-23) + +### Miscellaneous Chores + +* include wda sim prebuilt for gh release ([#1038](https://github.com/appium/WebDriverAgent/issues/1038)) ([4423ecb](https://github.com/appium/WebDriverAgent/commit/4423ecb4f23c50343d8ffbf56a7753b985cbab81)) + +## [9.14.3](https://github.com/appium/WebDriverAgent/compare/v9.14.2...v9.14.3) (2025-06-13) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 20.0.0 to 21.0.0 ([#1034](https://github.com/appium/WebDriverAgent/issues/1034)) ([5b205f4](https://github.com/appium/WebDriverAgent/commit/5b205f493f35cd1744cf9e33bce21e0f9e7c3bea)) + +## [9.14.2](https://github.com/appium/WebDriverAgent/compare/v9.14.1...v9.14.2) (2025-06-10) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 22.15.31 to 24.0.0 ([#1033](https://github.com/appium/WebDriverAgent/issues/1033)) ([e9705d9](https://github.com/appium/WebDriverAgent/commit/e9705d964e63222daaf0710bd3b860ca2ba6850f)) + +## [9.14.1](https://github.com/appium/WebDriverAgent/compare/v9.14.0...v9.14.1) (2025-06-09) + +### Miscellaneous Chores + +* add -Wno-reserved-identifier option ([#1032](https://github.com/appium/WebDriverAgent/issues/1032)) ([005dc21](https://github.com/appium/WebDriverAgent/commit/005dc216d9f41757763fe5b1714b68697fa8ee30)) + +## [9.14.0](https://github.com/appium/WebDriverAgent/compare/v9.13.0...v9.14.0) (2025-06-09) + +### Features + +* add minimum and maximum value attributes to page source ([#1031](https://github.com/appium/WebDriverAgent/issues/1031)) ([0e4e7e7](https://github.com/appium/WebDriverAgent/commit/0e4e7e7c483b9196edae576481f4e37f99fc8705)) + +## [9.13.0](https://github.com/appium/WebDriverAgent/compare/v9.12.0...v9.13.0) (2025-06-05) + +### Features + +* expose nativeFrame attribute in XML page source ([#1029](https://github.com/appium/WebDriverAgent/issues/1029)) ([5b56a45](https://github.com/appium/WebDriverAgent/commit/5b56a453f836cbc4358ce24ae43032658467c35c)) + +## [9.12.0](https://github.com/appium/WebDriverAgent/compare/v9.11.0...v9.12.0) (2025-06-04) + +### Features + +* add accessibility traits to XML page source ([#1028](https://github.com/appium/WebDriverAgent/issues/1028)) ([2df6649](https://github.com/appium/WebDriverAgent/commit/2df6649cb532d65a8c14633591b76c90185644cb)) + +## [9.11.0](https://github.com/appium/WebDriverAgent/compare/v9.10.1...v9.11.0) (2025-06-03) + +### Features + +* Add includeHittableInSource setting for including real hittable attribute in XML source ([#1026](https://github.com/appium/WebDriverAgent/issues/1026)) ([0fa4e74](https://github.com/appium/WebDriverAgent/commit/0fa4e7417404b5975445d381d111753fe681edd4)) + +## [9.10.1](https://github.com/appium/WebDriverAgent/compare/v9.10.0...v9.10.1) (2025-05-30) + +### Miscellaneous Chores + +* Make sure the same import style is used everywhere ([#1024](https://github.com/appium/WebDriverAgent/issues/1024)) ([1c50072](https://github.com/appium/WebDriverAgent/commit/1c50072457a8b82eec3684029386ccfa9432eccc)) + +## [9.10.0](https://github.com/appium/WebDriverAgent/compare/v9.9.0...v9.10.0) (2025-05-27) + +### Features + +* Add accessibility traits of the element ([#1020](https://github.com/appium/WebDriverAgent/issues/1020)) ([9465aaf](https://github.com/appium/WebDriverAgent/commit/9465aafd5e81ef57be7f78e9f2e188d3c1ba1bee)) + +### Bug Fixes + +* Use native snapshots if hittable attribute is requested in xPath ([#1023](https://github.com/appium/WebDriverAgent/issues/1023)) ([49d26cb](https://github.com/appium/WebDriverAgent/commit/49d26cb02a8515d1a1b52b65b7cb65512dfd749b)) + +## [9.9.0](https://github.com/appium/WebDriverAgent/compare/v9.8.0...v9.9.0) (2025-05-26) + +### Features + +* Use another snapshotting mechanism for the hittable attribute calculation ([#1022](https://github.com/appium/WebDriverAgent/issues/1022)) ([13c9f45](https://github.com/appium/WebDriverAgent/commit/13c9f453d890ad9b78fa7c47728ebae33880966a)) + +## [9.8.0](https://github.com/appium/WebDriverAgent/compare/v9.7.1...v9.8.0) (2025-05-21) + +### Features + +* Add a native frame property of the element ([#1017](https://github.com/appium/WebDriverAgent/issues/1017)) ([09214c4](https://github.com/appium/WebDriverAgent/commit/09214c4228ed5a49c02adead452cb0bb8dd83b6d)) + +## [9.7.1](https://github.com/appium/WebDriverAgent/compare/v9.7.0...v9.7.1) (2025-05-21) + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#1019](https://github.com/appium/WebDriverAgent/issues/1019)) ([7108f7f](https://github.com/appium/WebDriverAgent/commit/7108f7f79575a1758bc7f05bd4ef790fd7694784)) + +## [9.7.0](https://github.com/appium/WebDriverAgent/compare/v9.6.3...v9.7.0) (2025-05-20) + +### Features + +* add placeholderValue to page source tree ([#1016](https://github.com/appium/WebDriverAgent/issues/1016)) ([509c207](https://github.com/appium/WebDriverAgent/commit/509c207b1366dd8582ba273edcdf77bfb30f53c9)) + +## [9.6.3](https://github.com/appium/WebDriverAgent/compare/v9.6.2...v9.6.3) (2025-05-18) + +### Miscellaneous Chores + +* Move the FBDoesElementSupportInnerText helper to a separate utility file ([#1018](https://github.com/appium/WebDriverAgent/issues/1018)) ([f17b07d](https://github.com/appium/WebDriverAgent/commit/f17b07d03abb6c2100405fda04326b7c35bfb48b)) + +## [9.6.2](https://github.com/appium/WebDriverAgent/compare/v9.6.1...v9.6.2) (2025-05-01) + +### Bug Fixes + +* release element screenshot data ([#1013](https://github.com/appium/WebDriverAgent/issues/1013)) ([a85f327](https://github.com/appium/WebDriverAgent/commit/a85f3271991556941234fbc888528051b1569db1)) + +## [9.6.1](https://github.com/appium/WebDriverAgent/compare/v9.6.0...v9.6.1) (2025-04-22) + +### Bug Fixes + +* allow setting precise resolution for the MJPEG stream ([#1009](https://github.com/appium/WebDriverAgent/issues/1009)) ([3f86eda](https://github.com/appium/WebDriverAgent/commit/3f86edafda42d955929f7cca870e2b8da54ae930)) + +## [9.6.0](https://github.com/appium/WebDriverAgent/compare/v9.5.2...v9.6.0) (2025-04-20) + +### Features + +* Split custom and standard snapshotting methods ([#1008](https://github.com/appium/WebDriverAgent/issues/1008)) ([8358856](https://github.com/appium/WebDriverAgent/commit/8358856f5968977b13d5cbdafac97f3053dae56e)) + +## [9.5.2](https://github.com/appium/WebDriverAgent/compare/v9.5.1...v9.5.2) (2025-04-19) + +### Bug Fixes + +* Missing text in long text for get text/value ([#1007](https://github.com/appium/WebDriverAgent/issues/1007)) ([6603a0b](https://github.com/appium/WebDriverAgent/commit/6603a0ba384917d39389509958ccac03ad174610)) + +## [9.5.1](https://github.com/appium/WebDriverAgent/compare/v9.5.0...v9.5.1) (2025-04-10) + +### Bug Fixes + +* Make sure we don't store element snapshot in the cache ([#1001](https://github.com/appium/WebDriverAgent/issues/1001)) ([cfe052b](https://github.com/appium/WebDriverAgent/commit/cfe052bb3adb3f3b24d0a34f386c60cf1516b308)) + +## [9.5.0](https://github.com/appium/WebDriverAgent/compare/v9.4.1...v9.5.0) (2025-04-10) + +### Features + +* Add support for the autoClickAlertSelector setting ([#1002](https://github.com/appium/WebDriverAgent/issues/1002)) ([fd31b95](https://github.com/appium/WebDriverAgent/commit/fd31b9589199d0a7bc76919f6aa7c7c74c498b90)) + +## [9.4.1](https://github.com/appium/WebDriverAgent/compare/v9.4.0...v9.4.1) (2025-04-05) + +### Miscellaneous Chores + +* bump appium-ios-simulator ([445741d](https://github.com/appium/WebDriverAgent/commit/445741d03313019016d4232f49e656d50f673f16)) + +## [9.4.0](https://github.com/appium/WebDriverAgent/compare/v9.3.3...v9.4.0) (2025-04-02) + +### Features + +* Always apply the native snapshotting strategy for XCUIApplication instances ([#998](https://github.com/appium/WebDriverAgent/issues/998)) ([60f5aef](https://github.com/appium/WebDriverAgent/commit/60f5aeffdda85faffd60aba416dc9d92987f19ac)) + +## [9.3.3](https://github.com/appium/WebDriverAgent/compare/v9.3.2...v9.3.3) (2025-03-27) + +### Bug Fixes + +* Properly set snapshot lookup scope if limitXpathContextScope is disabled ([#996](https://github.com/appium/WebDriverAgent/issues/996)) ([03ca7cd](https://github.com/appium/WebDriverAgent/commit/03ca7cd27b7cd92a45b344eb661db973c5dde809)) + +## [9.3.2](https://github.com/appium/WebDriverAgent/compare/v9.3.1...v9.3.2) (2025-03-26) + +### Bug Fixes + +* Adjust limitXPathContextScope setting name ([#995](https://github.com/appium/WebDriverAgent/issues/995)) ([9789e39](https://github.com/appium/WebDriverAgent/commit/9789e393b55bc682a9a8ef5a65fba5e4dbf752ce)) + +## [9.3.1](https://github.com/appium/WebDriverAgent/compare/v9.3.0...v9.3.1) (2025-03-25) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#994](https://github.com/appium/WebDriverAgent/issues/994)) ([f55462f](https://github.com/appium/WebDriverAgent/commit/f55462f4fa63314dfea48670d17ee54dc5fe2d96)) + +## [9.3.0](https://github.com/appium/WebDriverAgent/compare/v9.2.0...v9.3.0) (2025-03-21) + +### Features + +* Add /window/rect W3C endpoint ([#991](https://github.com/appium/WebDriverAgent/issues/991)) ([34f9510](https://github.com/appium/WebDriverAgent/commit/34f95107997bdec63219a2fd917de899de3e198c)) + +## [9.2.0](https://github.com/appium/WebDriverAgent/compare/v9.1.0...v9.2.0) (2025-03-13) + +### Features + +* Add 'limitXpathContextScope' setting ([#988](https://github.com/appium/WebDriverAgent/issues/988)) ([9c9d8af](https://github.com/appium/WebDriverAgent/commit/9c9d8af9c98ba7b2843a42f54354b78e126d2d27)) + +## [9.1.0](https://github.com/appium/WebDriverAgent/compare/v9.0.6...v9.1.0) (2025-03-09) + +### Features + +* add placeholderValue ([#987](https://github.com/appium/WebDriverAgent/issues/987)) ([8c3a1cb](https://github.com/appium/WebDriverAgent/commit/8c3a1cb30655ed8d1a77d25bbeca71ee48c2ec3e)) + +## [9.0.6](https://github.com/appium/WebDriverAgent/compare/v9.0.5...v9.0.6) (2025-02-28) + +### Bug Fixes + +* optimize LRU cache ([#985](https://github.com/appium/WebDriverAgent/issues/985)) ([46dc417](https://github.com/appium/WebDriverAgent/commit/46dc417da9f4a843838b414c0b154d6f478dbc0b)) + +## [9.0.5](https://github.com/appium/WebDriverAgent/compare/v9.0.4...v9.0.5) (2025-02-26) + +### Bug Fixes + +* add autorelease pool to drain temporary objects ([#983](https://github.com/appium/WebDriverAgent/issues/983)) ([f92f1cd](https://github.com/appium/WebDriverAgent/commit/f92f1cde0fe914086103a110844bbe3bc0e3c4a6)) + +## [9.0.4](https://github.com/appium/WebDriverAgent/compare/v9.0.3...v9.0.4) (2025-02-21) + +### Bug Fixes + +* Accept reqBasePath proxy option ([#982](https://github.com/appium/WebDriverAgent/issues/982)) ([19efbdd](https://github.com/appium/WebDriverAgent/commit/19efbdd69ff9edff20c0c318bd39c29963d4d51d)) + +## [9.0.3](https://github.com/appium/WebDriverAgent/compare/v9.0.2...v9.0.3) (2025-02-05) + +### Bug Fixes + +* add nullable signature ([#979](https://github.com/appium/WebDriverAgent/issues/979)) ([34b303c](https://github.com/appium/WebDriverAgent/commit/34b303c4e226d6a75a45a14eee7ca5e253e67737)) + +## [9.0.2](https://github.com/appium/WebDriverAgent/compare/v9.0.1...v9.0.2) (2025-02-03) + +### Bug Fixes + +* update docs link in xcodebuild error message ([#978](https://github.com/appium/WebDriverAgent/issues/978)) ([ea3863a](https://github.com/appium/WebDriverAgent/commit/ea3863a67d5cfa8bc2e48a1dc2c59052acd47937)) + +## [9.0.1](https://github.com/appium/WebDriverAgent/compare/v9.0.0...v9.0.1) (2025-01-17) + +### Miscellaneous Chores + +* Optimize stable instance retrieval ([#973](https://github.com/appium/WebDriverAgent/issues/973)) ([f2c752d](https://github.com/appium/WebDriverAgent/commit/f2c752db4707b3864efb62b95b64abb487d28e4b)) + +## [9.0.0](https://github.com/appium/WebDriverAgent/compare/v8.12.2...v9.0.0) (2025-01-16) + +### ⚠ BREAKING CHANGES + +* snapshotTimeout and customSnapshotTimeout settings have been removed as a result of the custom snapshotting logic removal + +### Features + +* Refactor snapshotting mechanism ([#970](https://github.com/appium/WebDriverAgent/issues/970)) ([08f1306](https://github.com/appium/WebDriverAgent/commit/08f13060119c710f53b34a98c95683287c0365a0)) + +## [8.12.2](https://github.com/appium/WebDriverAgent/compare/v8.12.1...v8.12.2) (2025-01-13) + +### Miscellaneous Chores + +* Exclude element visibility and accessibility info from the accessibility audit details ([#968](https://github.com/appium/WebDriverAgent/issues/968)) ([f62afc3](https://github.com/appium/WebDriverAgent/commit/f62afc372c123bdd8dd7bb493f653bb128144d24)) + +## [8.12.1](https://github.com/appium/WebDriverAgent/compare/v8.12.0...v8.12.1) (2025-01-03) + +### Miscellaneous Chores + +* Bump eslint ([#965](https://github.com/appium/WebDriverAgent/issues/965)) ([17f49ec](https://github.com/appium/WebDriverAgent/commit/17f49ec5a54e97b0ef0d20a3e39fc96b32575e43)) + +## [8.12.0](https://github.com/appium/WebDriverAgent/compare/v8.11.3...v8.12.0) (2024-12-13) + +### Features + +* look for critical notification in respectSystemAlerts ([#962](https://github.com/appium/WebDriverAgent/issues/962)) ([916c8c5](https://github.com/appium/WebDriverAgent/commit/916c8c557a9366608df211f33b5b7fbb0354dad3)) + +## [8.11.3](https://github.com/appium/WebDriverAgent/compare/v8.11.2...v8.11.3) (2024-12-06) + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 5.1.8 to 6.0.0 ([#960](https://github.com/appium/WebDriverAgent/issues/960)) ([dbeb09c](https://github.com/appium/WebDriverAgent/commit/dbeb09c89f8c02e00a7bdffe7899650d435f3575)) + +## [8.11.2](https://github.com/appium/WebDriverAgent/compare/v8.11.1...v8.11.2) (2024-12-03) + +### Miscellaneous Chores + +* **deps-dev:** bump mocha from 10.8.2 to 11.0.1 ([#959](https://github.com/appium/WebDriverAgent/issues/959)) ([55b49c8](https://github.com/appium/WebDriverAgent/commit/55b49c83581c9e88f70806d98015238de3104f19)) + +## [8.11.1](https://github.com/appium/WebDriverAgent/compare/v8.11.0...v8.11.1) (2024-11-11) + +### Miscellaneous Chores + +* bump appium-ios-device ([#955](https://github.com/appium/WebDriverAgent/issues/955)) ([021f349](https://github.com/appium/WebDriverAgent/commit/021f34901866f4a7870914c00781b83bd0cbddc4)) + +## [8.11.0](https://github.com/appium/WebDriverAgent/compare/v8.10.1...v8.11.0) (2024-11-11) + +### Features + +* Add support for excluded_attributes in JSON source hierarchy ([#953](https://github.com/appium/WebDriverAgent/issues/953)) ([6112223](https://github.com/appium/WebDriverAgent/commit/6112223b21026fae5545fe1b1433a09c67ff524b)) + +## [8.10.1](https://github.com/appium/WebDriverAgent/compare/v8.10.0...v8.10.1) (2024-11-10) + +### Miscellaneous Chores + +* remove unnecessary lines ([#954](https://github.com/appium/WebDriverAgent/issues/954)) ([940df80](https://github.com/appium/WebDriverAgent/commit/940df80937381b481a2762fbf86b6249804591bd)) + +## [8.10.0](https://github.com/appium/WebDriverAgent/compare/v8.9.4...v8.10.0) (2024-11-07) + +### Features + +* add useClearTextShortcut setting ([#952](https://github.com/appium/WebDriverAgent/issues/952)) ([61bc051](https://github.com/appium/WebDriverAgent/commit/61bc051180d691d26233c66a5a76ed20b7fa09d2)) + +## [8.9.4](https://github.com/appium/WebDriverAgent/compare/v8.9.3...v8.9.4) (2024-10-17) + +### Bug Fixes + +* Consider transient overlay windows when respectSystemAlerts is enabled ([#946](https://github.com/appium/WebDriverAgent/issues/946)) ([f0bdce7](https://github.com/appium/WebDriverAgent/commit/f0bdce7eb8fdb13d2309d28e936950c77f006b20)) + +## [8.9.3](https://github.com/appium/WebDriverAgent/compare/v8.9.2...v8.9.3) (2024-10-07) + +### Miscellaneous Chores + +* remove unused FBBaseActionsParser and cleanup imports in FBConfiguration ([#943](https://github.com/appium/WebDriverAgent/issues/943)) ([a2173d0](https://github.com/appium/WebDriverAgent/commit/a2173d05df8ef831310e805a8e6a8a8d17725201)) + +## [8.9.2](https://github.com/appium/WebDriverAgent/compare/v8.9.1...v8.9.2) (2024-09-13) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 18.0.1 to 19.0.1 ([#938](https://github.com/appium/WebDriverAgent/issues/938)) ([3ef0093](https://github.com/appium/WebDriverAgent/commit/3ef009317801dca47efe34bd048d3cab2e644ee2)) + +## [8.9.1](https://github.com/appium/WebDriverAgent/compare/v8.9.0...v8.9.1) (2024-08-09) + +### Bug Fixes + +* Update swizzling of waitForQuiescenceIncludingAnimationsIdle: API for Xcode16-beta5 ([#935](https://github.com/appium/WebDriverAgent/issues/935)) ([2ccc436](https://github.com/appium/WebDriverAgent/commit/2ccc436991ca880a1dfdec688dc8167008fe382d)) + +## [8.9.0](https://github.com/appium/WebDriverAgent/compare/v8.8.0...v8.9.0) (2024-08-07) + +### Features + +* Add idleTimeoutMs param to the openUrl call ([#933](https://github.com/appium/WebDriverAgent/issues/933)) ([5e98841](https://github.com/appium/WebDriverAgent/commit/5e98841f56eda6454d67d813b921bfcf98f1ff78)) + +### Bug Fixes + +* Revert the logic to open the default URL in Safari via deeplink ([#932](https://github.com/appium/WebDriverAgent/issues/932)) ([7c51145](https://github.com/appium/WebDriverAgent/commit/7c5114518509c9a399845283eca7708248fb838f)) + +## [8.8.0](https://github.com/appium/WebDriverAgent/compare/v8.7.12...v8.8.0) (2024-08-06) + +### Features + +* Open the default URL in Safari upon session startup ([#929](https://github.com/appium/WebDriverAgent/issues/929)) ([97cf91d](https://github.com/appium/WebDriverAgent/commit/97cf91de34dc53e5f75f91829dc43224101c1b45)) + +## [8.7.12](https://github.com/appium/WebDriverAgent/compare/v8.7.11...v8.7.12) (2024-08-02) + +### Miscellaneous Chores + +* Replace fancy-log dependency with appium logger ([#928](https://github.com/appium/WebDriverAgent/issues/928)) ([5d2ec24](https://github.com/appium/WebDriverAgent/commit/5d2ec249488655451e2d46384e560fee7e08e840)) + +## [8.7.11](https://github.com/appium/WebDriverAgent/compare/v8.7.10...v8.7.11) (2024-07-29) + +### Bug Fixes + +* Respond to /health with a proper HTML ([#925](https://github.com/appium/WebDriverAgent/issues/925)) ([42c519f](https://github.com/appium/WebDriverAgent/commit/42c519f9df7beec81175fd38af388975d6f6b800)) + +## [8.7.10](https://github.com/appium/WebDriverAgent/compare/v8.7.9...v8.7.10) (2024-07-29) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 20.14.13 to 22.0.0 ([#926](https://github.com/appium/WebDriverAgent/issues/926)) ([1699023](https://github.com/appium/WebDriverAgent/commit/1699023086a243c3d86ddae4da8342c6beda3f48)) + +## [8.7.9](https://github.com/appium/WebDriverAgent/compare/v8.7.8...v8.7.9) (2024-07-21) + +### Miscellaneous Chores + +* keep error handling for the future possible usage ([#921](https://github.com/appium/WebDriverAgent/issues/921)) ([2f90739](https://github.com/appium/WebDriverAgent/commit/2f90739340d70073b48c703b36b9a313d3618972)) + +## [8.7.8](https://github.com/appium/WebDriverAgent/compare/v8.7.7...v8.7.8) (2024-07-18) + +### Bug Fixes + +* do nothing for an empty array in w3c actions ([#919](https://github.com/appium/WebDriverAgent/issues/919)) ([9e70ec1](https://github.com/appium/WebDriverAgent/commit/9e70ec1dbec1d1844278a58297a5b956ebaeb7fc)) + +## [8.7.7](https://github.com/appium/WebDriverAgent/compare/v8.7.6...v8.7.7) (2024-07-18) + +### Bug Fixes + +* Pass-through modifier keys ([#918](https://github.com/appium/WebDriverAgent/issues/918)) ([29d0e5c](https://github.com/appium/WebDriverAgent/commit/29d0e5cb2a19809e1babb06e5adaa49b43c754a5)) + +## [8.7.6](https://github.com/appium/WebDriverAgent/compare/v8.7.5...v8.7.6) (2024-07-02) + +### Miscellaneous Chores + +* Simplify xcodebuild lines monitoring ([#916](https://github.com/appium/WebDriverAgent/issues/916)) ([87678f2](https://github.com/appium/WebDriverAgent/commit/87678f260c98b3a3bc3d37017e9ef39098ccb3c4)) + +## [8.7.5](https://github.com/appium/WebDriverAgent/compare/v8.7.4...v8.7.5) (2024-06-26) + +### Bug Fixes + +* Respect wdaRemotePort capability for real devices ([#915](https://github.com/appium/WebDriverAgent/issues/915)) ([03ea143](https://github.com/appium/WebDriverAgent/commit/03ea1439a9cc5b6495be60707bc474e3ae9bdb06)) + +## [8.7.4](https://github.com/appium/WebDriverAgent/compare/v8.7.3...v8.7.4) (2024-06-20) + +### Miscellaneous Chores + +* Bump chai and chai-as-promised ([#913](https://github.com/appium/WebDriverAgent/issues/913)) ([9086783](https://github.com/appium/WebDriverAgent/commit/90867832ec3077f0036938aa68a168a5702fc90a)) + +## [8.7.3](https://github.com/appium/WebDriverAgent/compare/v8.7.2...v8.7.3) (2024-06-12) + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 4.5.0 to 5.0.3 ([#910](https://github.com/appium/WebDriverAgent/issues/910)) ([936005b](https://github.com/appium/WebDriverAgent/commit/936005b458e7b5b64b60d9bda37d45bb5a90e615)) + +## [8.7.2](https://github.com/appium/WebDriverAgent/compare/v8.7.1...v8.7.2) (2024-06-04) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 17.0.2 to 18.0.0 ([#903](https://github.com/appium/WebDriverAgent/issues/903)) ([87e4ba5](https://github.com/appium/WebDriverAgent/commit/87e4ba5ce3868d99ac889795039936be119ef87a)) + +## [8.7.1](https://github.com/appium/WebDriverAgent/compare/v8.7.0...v8.7.1) (2024-06-04) + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 23.1.1 to 24.0.0 and conventional-changelog-conventionalcommits to 8.0.0 ([#908](https://github.com/appium/WebDriverAgent/issues/908)) ([26019ec](https://github.com/appium/WebDriverAgent/commit/26019eca9b7331353e26a1014bc4afcecc0450f3)) + +## [8.7.0](https://github.com/appium/WebDriverAgent/compare/v8.6.0...v8.7.0) (2024-06-01) + + +### Features + +* Add a setting to respect system alerts while detecting active apps ([#907](https://github.com/appium/WebDriverAgent/issues/907)) ([5c82d66](https://github.com/appium/WebDriverAgent/commit/5c82d66890b1a74f9b6f698c87590b2154a6c1bd)) + +## [8.6.0](https://github.com/appium/WebDriverAgent/compare/v8.5.7...v8.6.0) (2024-05-17) + + +### Features + +* support maxTypingFrequency in settings api ([#904](https://github.com/appium/WebDriverAgent/issues/904)) ([fa4776a](https://github.com/appium/WebDriverAgent/commit/fa4776a2bfa15cbec8bba35d8ed11318d9629934)) + +## [8.5.7](https://github.com/appium/WebDriverAgent/compare/v8.5.6...v8.5.7) (2024-05-16) + + +### Miscellaneous Chores + +* Update dev dependencies ([e49dcf2](https://github.com/appium/WebDriverAgent/commit/e49dcf2afb0a10edc7085ac56d297234c00d57b0)) + +## [8.5.6](https://github.com/appium/WebDriverAgent/compare/v8.5.5...v8.5.6) (2024-04-20) + + +### Bug Fixes + +* unit test for linux ([#894](https://github.com/appium/WebDriverAgent/issues/894)) ([3a90158](https://github.com/appium/WebDriverAgent/commit/3a9015898d70b177cb6cbfcaf412dfa3c4ec3865)) + +## [8.5.5](https://github.com/appium/WebDriverAgent/compare/v8.5.4...v8.5.5) (2024-04-20) + + +### Bug Fixes + +* xcode warning about com.facebook.wda.lib ([#892](https://github.com/appium/WebDriverAgent/issues/892)) ([6398079](https://github.com/appium/WebDriverAgent/commit/63980796d8f40bd68ffb5af4b085a2348e544a13)) + +## [8.5.4](https://github.com/appium/WebDriverAgent/compare/v8.5.3...v8.5.4) (2024-04-20) + + +### Miscellaneous Chores + +* remove old iOS/Xcode related test code and errors ([#890](https://github.com/appium/WebDriverAgent/issues/890)) ([2fd0dea](https://github.com/appium/WebDriverAgent/commit/2fd0dead0c86d6be08e040360dec9ea085ba0392)) + +## [8.5.3](https://github.com/appium/WebDriverAgent/compare/v8.5.2...v8.5.3) (2024-04-19) + + +### Miscellaneous Chores + +* update integerationapp for newer OS env ([#891](https://github.com/appium/WebDriverAgent/issues/891)) ([2c78348](https://github.com/appium/WebDriverAgent/commit/2c7834842afeb1aec77e953ce11ac3c43c839431)) + +## [8.5.2](https://github.com/appium/WebDriverAgent/compare/v8.5.1...v8.5.2) (2024-04-09) + + +### Miscellaneous Chores + +* **deps-dev:** bump @typescript-eslint/parser from 6.21.0 to 7.6.0 ([#888](https://github.com/appium/WebDriverAgent/issues/888)) ([ead75eb](https://github.com/appium/WebDriverAgent/commit/ead75eb87a5c8e94088bace8f372ab137dcf57ad)) +* Remove extra imports ([fb25742](https://github.com/appium/WebDriverAgent/commit/fb25742a07a2fbcb0365a48d54117267c7c916df)) + +## [8.5.1](https://github.com/appium/WebDriverAgent/compare/v8.5.0...v8.5.1) (2024-04-08) + + +### Miscellaneous Chores + +* Add more type declarations ([#886](https://github.com/appium/WebDriverAgent/issues/886)) ([9ca7632](https://github.com/appium/WebDriverAgent/commit/9ca7632faf999931e7f5edf47267fcce6d6392b2)) + +## [8.5.0](https://github.com/appium/WebDriverAgent/compare/v8.4.0...v8.5.0) (2024-04-07) + + +### Features + +* Add types for WDA caps and settings ([#885](https://github.com/appium/WebDriverAgent/issues/885)) ([4b3c220](https://github.com/appium/WebDriverAgent/commit/4b3c220c0c609802924b7b6ff9a4dfa7a98eb5f4)) + +## [8.4.0](https://github.com/appium/WebDriverAgent/compare/v8.3.1...v8.4.0) (2024-04-01) + + +### Features + +* add system screen size/width in the system info endpoint ([#881](https://github.com/appium/WebDriverAgent/issues/881)) ([5ebc71c](https://github.com/appium/WebDriverAgent/commit/5ebc71c6ca2b364d44a44716e794885f8d3b6d9c)) + +## [8.3.1](https://github.com/appium/WebDriverAgent/compare/v8.3.0...v8.3.1) (2024-03-31) + + +### Miscellaneous Chores + +* do not cleanup with this.usePrebuiltWDA ([#882](https://github.com/appium/WebDriverAgent/issues/882)) ([0436e95](https://github.com/appium/WebDriverAgent/commit/0436e95752826bee7786577ac1bc0d056af11bc8)) + +## [8.3.0](https://github.com/appium/WebDriverAgent/compare/v8.2.1...v8.3.0) (2024-03-29) + + +### Features + +* Add module version to the /status output ([#878](https://github.com/appium/WebDriverAgent/issues/878)) ([a9603f8](https://github.com/appium/WebDriverAgent/commit/a9603f82acbdacdeb7a55b857512ba35353a4bc3)) + +## [8.2.1](https://github.com/appium/WebDriverAgent/compare/v8.2.0...v8.2.1) (2024-03-28) + + +### Miscellaneous Chores + +* wait for wda start in sim as well for preinstalled wda start ([#876](https://github.com/appium/WebDriverAgent/issues/876)) ([6c8920a](https://github.com/appium/WebDriverAgent/commit/6c8920adddb373b463259c3e6c14cb3c49ecbf2b)) + +## [8.2.0](https://github.com/appium/WebDriverAgent/compare/v8.1.0...v8.2.0) (2024-03-28) + + +### Features + +* Add a capability to customize the default state change timeout on app startup ([#877](https://github.com/appium/WebDriverAgent/issues/877)) ([98351c3](https://github.com/appium/WebDriverAgent/commit/98351c358367e67e63701612fd3702d53437e12e)) + +## [8.1.0](https://github.com/appium/WebDriverAgent/compare/v8.0.2...v8.1.0) (2024-03-26) + + +### Features + +* add updatedWDABundleIdSuffix to handle bundle id for updatedWDABundleId with usePreinstalledWDA ([#871](https://github.com/appium/WebDriverAgent/issues/871)) ([d79b624](https://github.com/appium/WebDriverAgent/commit/d79b6245966baaa57f7a1f785d7f9b4ea5a7f104)) + +## [8.0.2](https://github.com/appium/WebDriverAgent/compare/v8.0.1...v8.0.2) (2024-03-26) + + +### Miscellaneous Chores + +* **deps:** bump appium-ios-simulator from 5.5.3 to 6.0.0 ([#874](https://github.com/appium/WebDriverAgent/issues/874)) ([72f2a97](https://github.com/appium/WebDriverAgent/commit/72f2a97ec31dbb3c66e5f459e0d7fd417c197d5d)) + +## [8.0.1](https://github.com/appium/WebDriverAgent/compare/v8.0.0...v8.0.1) (2024-03-26) + + +### Miscellaneous Chores + +* use bundle id outside opts for this.device.devicectl.launchApp ([#872](https://github.com/appium/WebDriverAgent/issues/872)) ([e2aeda2](https://github.com/appium/WebDriverAgent/commit/e2aeda2f2020f4014cba478b459e47954175f597)) + +## [8.0.0](https://github.com/appium/WebDriverAgent/compare/v7.3.1...v8.0.0) (2024-03-25) + + +### ⚠ BREAKING CHANGES + +* calls launch app process command with devicectl via this.device.devicectl + +### Features + +* launch WDA via devicectl object ([#870](https://github.com/appium/WebDriverAgent/issues/870)) ([090b815](https://github.com/appium/WebDriverAgent/commit/090b815ae47e1ef0e0a9842fac6828346bc38fe6)) + +## [7.3.1](https://github.com/appium/WebDriverAgent/compare/v7.3.0...v7.3.1) (2024-03-24) + + +### Miscellaneous Chores + +* move node-simctl to dev deps ([#869](https://github.com/appium/WebDriverAgent/issues/869)) ([9033759](https://github.com/appium/WebDriverAgent/commit/90337597e6c480c790cf299e160bc53731c0a87d)) + +## [7.3.0](https://github.com/appium/WebDriverAgent/compare/v7.2.0...v7.3.0) (2024-03-23) + + +### Features + +* Support prebuiltWDAPath for iOS 17 ([#868](https://github.com/appium/WebDriverAgent/issues/868)) ([39194d4](https://github.com/appium/WebDriverAgent/commit/39194d4ac6d0072c1214088ff5c15c986969914c)) + +## [7.2.0](https://github.com/appium/WebDriverAgent/compare/v7.1.2...v7.2.0) (2024-03-21) + + +### Features + +* Enable usePreinstalledWDA feature for simulators ([#866](https://github.com/appium/WebDriverAgent/issues/866)) ([7c684e2](https://github.com/appium/WebDriverAgent/commit/7c684e2def9dd968de1cf89e4ec26403a52ba805)) + +## [7.1.2](https://github.com/appium/WebDriverAgent/compare/v7.1.1...v7.1.2) (2024-03-14) + + +### Bug Fixes + +* Always assume en0 is the WiFi interface ([#864](https://github.com/appium/WebDriverAgent/issues/864)) ([6dbfb3f](https://github.com/appium/WebDriverAgent/commit/6dbfb3f2ec8e0bfa5a42c6f8ab882893bfe3f534)) + +## [7.1.1](https://github.com/appium/WebDriverAgent/compare/v7.1.0...v7.1.1) (2024-03-13) + + +### Bug Fixes + +* respect defaultActiveApplication in activeApplication selection ([#862](https://github.com/appium/WebDriverAgent/issues/862)) ([b1ddae2](https://github.com/appium/WebDriverAgent/commit/b1ddae2be3fd3f7c87de79e804d82cf7c13dc56e)) + +## [7.1.0](https://github.com/appium/WebDriverAgent/compare/v7.0.6...v7.1.0) (2024-03-07) + + +### Features + +* Add wrappers for native XCTest video recorder ([#858](https://github.com/appium/WebDriverAgent/issues/858)) ([9728548](https://github.com/appium/WebDriverAgent/commit/9728548676c8de67c30d127ee8b0374f58286e74)) + + +### Miscellaneous Chores + +* bump typescript ([89880f5](https://github.com/appium/WebDriverAgent/commit/89880f509f930f16f6469bcda613569040c337b6)) + +## [7.0.6](https://github.com/appium/WebDriverAgent/compare/v7.0.5...v7.0.6) (2024-03-03) + + +### Miscellaneous Chores + +* Handle app startup errors as session creation exceptions ([#855](https://github.com/appium/WebDriverAgent/issues/855)) ([0ec5398](https://github.com/appium/WebDriverAgent/commit/0ec5398e9cb4b0e5ab133cc0c330b85b3d37766e)) + +## [7.0.5](https://github.com/appium/WebDriverAgent/compare/v7.0.4...v7.0.5) (2024-03-03) + + +### Reverts + +* Revert "chore: tune release packages (#856)" (#857) ([dc72015](https://github.com/appium/WebDriverAgent/commit/dc720157a60925451e6d5935abcd168082d44785)), closes [#856](https://github.com/appium/WebDriverAgent/issues/856) [#857](https://github.com/appium/WebDriverAgent/issues/857) + +## [7.0.4](https://github.com/appium/WebDriverAgent/compare/v7.0.3...v7.0.4) (2024-03-03) + + +### Miscellaneous Chores + +* dummy commit to trigger a release ([0cb66c5](https://github.com/appium/WebDriverAgent/commit/0cb66c5edc91c191d5ec412ba0a479e07cb4214b)) + +## [7.0.3](https://github.com/appium/WebDriverAgent/compare/v7.0.2...v7.0.3) (2024-03-03) + + +### Miscellaneous Chores + +* tune release packages ([#856](https://github.com/appium/WebDriverAgent/issues/856)) ([aa0765e](https://github.com/appium/WebDriverAgent/commit/aa0765e425faba6c035a9933320e91679b167b80)) + +## [7.0.2](https://github.com/appium/WebDriverAgent/compare/v7.0.1...v7.0.2) (2024-02-28) + + +### Miscellaneous Chores + +* Tune alert detection if system app is active ([#854](https://github.com/appium/WebDriverAgent/issues/854)) ([857d3de](https://github.com/appium/WebDriverAgent/commit/857d3decf497935098ba6acb61654be1da173b11)) + +## [7.0.1](https://github.com/appium/WebDriverAgent/compare/v7.0.0...v7.0.1) (2024-02-21) + + +### Miscellaneous Chores + +* Simplify the logic of alert element detection ([#851](https://github.com/appium/WebDriverAgent/issues/851)) ([54f91f1](https://github.com/appium/WebDriverAgent/commit/54f91f198e45535ea9d86b7eee40b21f43f84294)) + +## [7.0.0](https://github.com/appium/WebDriverAgent/compare/v6.1.1...v7.0.0) (2024-02-12) + + +### ⚠ BREAKING CHANGES + +* The following REST endpoints have been removed, use W3C actions instead: +- /wda/touch/perform +- /wda/touch/multi/perform + +### Features + +* Remove obsolete MJSONWP touch actions ([#847](https://github.com/appium/WebDriverAgent/issues/847)) ([d77f640](https://github.com/appium/WebDriverAgent/commit/d77f640867155fddbbbc9575f0a77802602865e7)) + +## [6.1.1](https://github.com/appium/WebDriverAgent/compare/v6.1.0...v6.1.1) (2024-02-11) + + +### Miscellaneous Chores + +* Make sure the app under test is restarted if opened from a deep link ([#846](https://github.com/appium/WebDriverAgent/issues/846)) ([88b0a5b](https://github.com/appium/WebDriverAgent/commit/88b0a5b0f8aefa05a7dc28d17faf62c229e0706f)) + +## [6.1.0](https://github.com/appium/WebDriverAgent/compare/v6.0.0...v6.1.0) (2024-02-10) + + +### Features + +* Add a possibility of starting a test with a deep link ([#845](https://github.com/appium/WebDriverAgent/issues/845)) ([aa25e49](https://github.com/appium/WebDriverAgent/commit/aa25e49fa9821960b08e9f4f3ea5891ebdf7d48d)) + +## [6.0.0](https://github.com/appium/WebDriverAgent/compare/v5.15.8...v6.0.0) (2024-01-31) + + +### ⚠ BREAKING CHANGES + +* The /wda/tap/:uuid endpoint has been replaced by /wda/element/:uuid/tap and /wda/tap ones + +### Features + +* Add coordinate-based APIs for gesture calls ([#843](https://github.com/appium/WebDriverAgent/issues/843)) ([feda373](https://github.com/appium/WebDriverAgent/commit/feda373b6147d3e87b29dceb871887c77febe76b)) + +## [5.15.8](https://github.com/appium/WebDriverAgent/compare/v5.15.7...v5.15.8) (2024-01-24) + + +### Bug Fixes + +* use arm64 naming for xctestrun ([#840](https://github.com/appium/WebDriverAgent/issues/840)) ([429e154](https://github.com/appium/WebDriverAgent/commit/429e154c28ab2f17685723b02c941efce03984d4)) + +## [5.15.7](https://github.com/appium/WebDriverAgent/compare/v5.15.6...v5.15.7) (2024-01-16) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 22.0.12 to 23.0.0 ([#836](https://github.com/appium/WebDriverAgent/issues/836)) ([a3ac2c5](https://github.com/appium/WebDriverAgent/commit/a3ac2c58786955507a34d0adcc4a53cd30f55014)) + + +### Code Refactoring + +* Ditch FBApplication in favour of XCUIApplication extensions ([#834](https://github.com/appium/WebDriverAgent/issues/834)) ([70a8d98](https://github.com/appium/WebDriverAgent/commit/70a8d98bc15d8fc615455be07fad9c37ff8d430b)) + +## [5.15.6](https://github.com/appium/WebDriverAgent/compare/v5.15.5...v5.15.6) (2024-01-06) + + +### Miscellaneous Chores + +* Update keyboard typing implementation ([#832](https://github.com/appium/WebDriverAgent/issues/832)) ([06cfb3b](https://github.com/appium/WebDriverAgent/commit/06cfb3b2b895a0bec681218fce658bdfcb4d13e9)) + +## [5.15.5](https://github.com/appium/WebDriverAgent/compare/v5.15.4...v5.15.5) (2023-12-13) + + +### Miscellaneous Chores + +* use appearance for get as well if available ([#825](https://github.com/appium/WebDriverAgent/issues/825)) ([89e233d](https://github.com/appium/WebDriverAgent/commit/89e233d8aef5a19491785fee0823fd8eddbd5fcc)) + +## [5.15.4](https://github.com/appium/WebDriverAgent/compare/v5.15.3...v5.15.4) (2023-12-07) + + +### Bug Fixes + +* set appearance in iOS 17+ ([#818](https://github.com/appium/WebDriverAgent/issues/818)) ([357a2cb](https://github.com/appium/WebDriverAgent/commit/357a2cbca106daf42bc892b251802bfa00895598)) + +## [5.15.3](https://github.com/appium/WebDriverAgent/compare/v5.15.2...v5.15.3) (2023-11-24) + + +### Miscellaneous Chores + +* Make xcodebuild error message more helpful ([#816](https://github.com/appium/WebDriverAgent/issues/816)) ([2d7fc03](https://github.com/appium/WebDriverAgent/commit/2d7fc0370b30e5e3adc9a13002fa95f607c4c160)) + +## [5.15.2](https://github.com/appium/WebDriverAgent/compare/v5.15.1...v5.15.2) (2023-11-23) + + +### Bug Fixes + +* fix run test ci ([#814](https://github.com/appium/WebDriverAgent/issues/814)) ([014d04d](https://github.com/appium/WebDriverAgent/commit/014d04df956e47fef67938b089511e80d344f007)) + + +### Miscellaneous Chores + +* a dummy commit to check a package release ([08388fd](https://github.com/appium/WebDriverAgent/commit/08388fd602ee9d588a8780e8d141d748813782ed)) + +## [5.15.1](https://github.com/appium/WebDriverAgent/compare/v5.15.0...v5.15.1) (2023-11-16) + + +### Bug Fixes + +* Content-Type of the MJPEG server ([b4704da](https://github.com/appium/WebDriverAgent/commit/b4704dafc4567e1f0dc8675facfc48a195aae4bf)) + + +### Code Refactoring + +* Optimize screenshots preprocessing ([#812](https://github.com/appium/WebDriverAgent/issues/812)) ([0b41757](https://github.com/appium/WebDriverAgent/commit/0b41757c0d21004afab32860b4e510d4bc426018)) + +## [5.15.0](https://github.com/appium/WebDriverAgent/compare/v5.14.0...v5.15.0) (2023-11-16) + + +### Features + +* Add element attributes to the performAccessibilityAudit output ([#808](https://github.com/appium/WebDriverAgent/issues/808)) ([0d7e4a6](https://github.com/appium/WebDriverAgent/commit/0d7e4a697adb7355279583eaa05118f396056e6f)) + +## [5.14.0](https://github.com/appium/WebDriverAgent/compare/v5.13.3...v5.14.0) (2023-11-10) + + +### Features + +* use khidusage_keyboardclear to `clear` for iOS/iPad as the 1st attempt, tune tvOS ([#811](https://github.com/appium/WebDriverAgent/issues/811)) ([dd093ea](https://github.com/appium/WebDriverAgent/commit/dd093ea0b7209c3d2f3d0b1fa7f3a7b58507dd2d)) + +## [5.13.3](https://github.com/appium/WebDriverAgent/compare/v5.13.2...v5.13.3) (2023-11-10) + + +### Bug Fixes + +* unrecognized selector sent to instance 0x2829adb20 error in clear ([#809](https://github.com/appium/WebDriverAgent/issues/809)) ([79832bc](https://github.com/appium/WebDriverAgent/commit/79832bc6c69e289091fbbb97aee6a1f1d17ca4c3)) + +## [5.13.2](https://github.com/appium/WebDriverAgent/compare/v5.13.1...v5.13.2) (2023-11-06) + + +### Miscellaneous Chores + +* **deps-dev:** bump @types/sinon from 10.0.20 to 17.0.0 ([#805](https://github.com/appium/WebDriverAgent/issues/805)) ([824f74c](https://github.com/appium/WebDriverAgent/commit/824f74c69769973858350bd5db0061510c546b09)) + +## [5.13.1](https://github.com/appium/WebDriverAgent/compare/v5.13.0...v5.13.1) (2023-11-01) + + +### Miscellaneous Chores + +* **deps:** bump asyncbox from 2.9.4 to 3.0.0 ([#803](https://github.com/appium/WebDriverAgent/issues/803)) ([0f2305d](https://github.com/appium/WebDriverAgent/commit/0f2305d2559dc0807d7df0d0e06f7fc3c549701c)) + +## [5.13.0](https://github.com/appium/WebDriverAgent/compare/v5.12.3...v5.13.0) (2023-10-31) + + +### Features + +* Add "elementDescription" property to audit issues containing the debug description of an element ([#802](https://github.com/appium/WebDriverAgent/issues/802)) ([9925af4](https://github.com/appium/WebDriverAgent/commit/9925af44ec5fbfb66e6f034dfd93a6c25de48661)) + +## [5.12.3](https://github.com/appium/WebDriverAgent/compare/v5.12.2...v5.12.3) (2023-10-31) + + +### Miscellaneous Chores + +* Return better error on WDA startup timeout ([#801](https://github.com/appium/WebDriverAgent/issues/801)) ([796d5e7](https://github.com/appium/WebDriverAgent/commit/796d5e743676b174221e27e739a0164f4b91533c)) + +## [5.12.2](https://github.com/appium/WebDriverAgent/compare/v5.12.1...v5.12.2) (2023-10-29) + + +### Miscellaneous Chores + +* return operation error in `handleKeyboardInput` ([#799](https://github.com/appium/WebDriverAgent/issues/799)) ([247ace6](https://github.com/appium/WebDriverAgent/commit/247ace68f373c09054fabc3be088061089946806)) + +## [5.12.1](https://github.com/appium/WebDriverAgent/compare/v5.12.0...v5.12.1) (2023-10-28) + + +### Bug Fixes + +* when 0 is given for handleKeyboardInput ([#798](https://github.com/appium/WebDriverAgent/issues/798)) ([58ebe8e](https://github.com/appium/WebDriverAgent/commit/58ebe8eb52966963ee30a5c066beb3bf9fed3161)) + +## [5.12.0](https://github.com/appium/WebDriverAgent/compare/v5.11.7...v5.12.0) (2023-10-26) + + +### Features + +* Add an endpoint for keyboard input ([#797](https://github.com/appium/WebDriverAgent/issues/797)) ([aaf70c9](https://github.com/appium/WebDriverAgent/commit/aaf70c9196e4dcb2073da151cda23b2b221d4dae)) + +## [5.11.7](https://github.com/appium/WebDriverAgent/compare/v5.11.6...v5.11.7) (2023-10-25) + + +### Miscellaneous Chores + +* **deps-dev:** bump @typescript-eslint/eslint-plugin from 5.62.0 to 6.9.0 ([#796](https://github.com/appium/WebDriverAgent/issues/796)) ([dabf141](https://github.com/appium/WebDriverAgent/commit/dabf141acd3186b1c27231ef52826fa42208c980)) + +## [5.11.6](https://github.com/appium/WebDriverAgent/compare/v5.11.5...v5.11.6) (2023-10-25) + + +### Miscellaneous Chores + +* disable debugger for wda ([#768](https://github.com/appium/WebDriverAgent/issues/768)) ([e2f4405](https://github.com/appium/WebDriverAgent/commit/e2f4405a3449f1f4d390eae06bf91a220e81b58b)) + +## [5.11.5](https://github.com/appium/WebDriverAgent/compare/v5.11.4...v5.11.5) (2023-10-23) + + +### Miscellaneous Chores + +* **deps-dev:** bump eslint-config-prettier from 8.10.0 to 9.0.0 ([#791](https://github.com/appium/WebDriverAgent/issues/791)) ([f130961](https://github.com/appium/WebDriverAgent/commit/f130961f189f2746d4a2b0a18105fc10203312ca)) +* **deps-dev:** bump lint-staged from 14.0.1 to 15.0.2 ([#792](https://github.com/appium/WebDriverAgent/issues/792)) ([440279d](https://github.com/appium/WebDriverAgent/commit/440279d4f6d069e440180faf4bee8e5dc1758787)) +* **deps-dev:** bump semantic-release from 21.1.2 to 22.0.5 ([#781](https://github.com/appium/WebDriverAgent/issues/781)) ([a967183](https://github.com/appium/WebDriverAgent/commit/a96718308dbd6b13feb30e6ce8f01a7d9b74b146)) + +## [5.11.4](https://github.com/appium/WebDriverAgent/compare/v5.11.3...v5.11.4) (2023-10-23) + + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 16.1.3 to 17.0.0 ([#795](https://github.com/appium/WebDriverAgent/issues/795)) ([4921899](https://github.com/appium/WebDriverAgent/commit/4921899d96800dbcd59a9c27ba793ad16d0c715b)) + +## [5.11.3](https://github.com/appium/WebDriverAgent/compare/v5.11.2...v5.11.3) (2023-10-21) + + +### Miscellaneous Chores + +* use PRODUCT_BUNDLE_IDENTIFIER to info.plist ([#794](https://github.com/appium/WebDriverAgent/issues/794)) ([543c498](https://github.com/appium/WebDriverAgent/commit/543c49860d2d35148bcbaa33e14d3e1dab058cef)) + +## [5.11.2](https://github.com/appium/WebDriverAgent/compare/v5.11.1...v5.11.2) (2023-10-19) + + +### Miscellaneous Chores + +* Use latest teen_process types ([895cdfc](https://github.com/appium/WebDriverAgent/commit/895cdfc1a316117bb7c8b5be0265b439c1e911bc)) + +## [5.11.1](https://github.com/appium/WebDriverAgent/compare/v5.11.0...v5.11.1) (2023-10-19) + + +### Miscellaneous Chores + +* Use latest types version ([123eefb](https://github.com/appium/WebDriverAgent/commit/123eefba5e5e30100cb3cdff09a516179f78afe7)) + +## [5.11.0](https://github.com/appium/WebDriverAgent/compare/v5.10.1...v5.11.0) (2023-10-05) + + +### Features + +* Add /calibrate endpoint ([#785](https://github.com/appium/WebDriverAgent/issues/785)) ([ae1603a](https://github.com/appium/WebDriverAgent/commit/ae1603a3b5b5c4828ed4959c63d6274254f832a2)) + +## [5.10.1](https://github.com/appium/WebDriverAgent/compare/v5.10.0...v5.10.1) (2023-10-05) + + +### Miscellaneous Chores + +* Remove the hardcoded keyboard wait delay from handleKeys ([#784](https://github.com/appium/WebDriverAgent/issues/784)) ([f043d67](https://github.com/appium/WebDriverAgent/commit/f043d67dd90fbfca00b8cf53ccae63dbd67fa150)) + +## [5.10.0](https://github.com/appium/WebDriverAgent/compare/v5.9.1...v5.10.0) (2023-09-25) + + +### Features + +* remove test frameworks in Frameworks and add device local references as rpath for real devices ([#780](https://github.com/appium/WebDriverAgent/issues/780)) ([ae6c842](https://github.com/appium/WebDriverAgent/commit/ae6c842f3c4e7deb51fcc7a1a1045d4eeede69fd)) + +## [5.9.1](https://github.com/appium/WebDriverAgent/compare/v5.9.0...v5.9.1) (2023-09-22) + + +### Bug Fixes + +* Provide signing arguments as command line parameters ([#779](https://github.com/appium/WebDriverAgent/issues/779)) ([51ba527](https://github.com/appium/WebDriverAgent/commit/51ba527b6cde3773ebcd5323cfa7e0890b2563aa)) + +## [5.9.0](https://github.com/appium/WebDriverAgent/compare/v5.8.7...v5.9.0) (2023-09-22) + + +### Features + +* do not get active process information in a new session request ([#774](https://github.com/appium/WebDriverAgent/issues/774)) ([2784ce4](https://github.com/appium/WebDriverAgent/commit/2784ce440f8b5ab9710db08d9ffda704697ac07c)) + +## [5.8.7](https://github.com/appium/WebDriverAgent/compare/v5.8.6...v5.8.7) (2023-09-22) + + +### Miscellaneous Chores + +* tweak device in currentCapabilities ([#773](https://github.com/appium/WebDriverAgent/issues/773)) ([8481b02](https://github.com/appium/WebDriverAgent/commit/8481b02fc84de1147e1254ea7fd114f8735b0226)) + +## [5.8.6](https://github.com/appium/WebDriverAgent/compare/v5.8.5...v5.8.6) (2023-09-21) + + +### Miscellaneous Chores + +* add log to leave it in the system log ([#772](https://github.com/appium/WebDriverAgent/issues/772)) ([012af21](https://github.com/appium/WebDriverAgent/commit/012af21383829397c7265daa0513829cc4e93aee)) + +## [5.8.5](https://github.com/appium/WebDriverAgent/compare/v5.8.4...v5.8.5) (2023-09-15) + + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 15.2.0 to 16.0.0 ([#766](https://github.com/appium/WebDriverAgent/issues/766)) ([2ffd187](https://github.com/appium/WebDriverAgent/commit/2ffd187b2e8b3c1ed04537320179bdfe9f9635df)) + +## [5.8.4](https://github.com/appium/WebDriverAgent/compare/v5.8.3...v5.8.4) (2023-09-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump @types/teen_process from 2.0.0 to 2.0.1 ([#765](https://github.com/appium/WebDriverAgent/issues/765)) ([1af64b8](https://github.com/appium/WebDriverAgent/commit/1af64b8834371a3fdb3d0aab82fdfdeff6194555)) + +## [5.8.3](https://github.com/appium/WebDriverAgent/compare/v5.8.2...v5.8.3) (2023-09-01) + + +### Bug Fixes + +* Address some typing-related issues ([#759](https://github.com/appium/WebDriverAgent/issues/759)) ([87e8704](https://github.com/appium/WebDriverAgent/commit/87e87044d6216513f755c5184d61514a76cb0179)) + +## [5.8.2](https://github.com/appium/WebDriverAgent/compare/v5.8.1...v5.8.2) (2023-08-28) + + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#757](https://github.com/appium/WebDriverAgent/issues/757)) ([a3047ea](https://github.com/appium/WebDriverAgent/commit/a3047ea70b7a9fd5ccb2a2c93b0964d7de609d38)) + +## [5.8.1](https://github.com/appium/WebDriverAgent/compare/v5.8.0...v5.8.1) (2023-08-25) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 20.1.3 to 21.1.0 ([#754](https://github.com/appium/WebDriverAgent/issues/754)) ([d86d9a6](https://github.com/appium/WebDriverAgent/commit/d86d9a64ca75ad40273cfa10855f49b967d9fd95)) + +## [5.8.0](https://github.com/appium/WebDriverAgent/compare/v5.7.0...v5.8.0) (2023-08-24) + + +### Features + +* Add wdHittable property ([#756](https://github.com/appium/WebDriverAgent/issues/756)) ([075298b](https://github.com/appium/WebDriverAgent/commit/075298b286c83ab5d4a2855e9e0bb915790b3f43)) + +## [5.7.0](https://github.com/appium/WebDriverAgent/compare/v5.6.2...v5.7.0) (2023-08-24) + + +### Features + +* Switch babel to typescript ([#753](https://github.com/appium/WebDriverAgent/issues/753)) ([76a4c7f](https://github.com/appium/WebDriverAgent/commit/76a4c7f066e1895acbb153ab035d6a08604277e4)) + +## [5.6.2](https://github.com/appium/WebDriverAgent/compare/v5.6.1...v5.6.2) (2023-08-23) + + +### Miscellaneous Chores + +* Remove unused glob dependency ([ee7655e](https://github.com/appium/WebDriverAgent/commit/ee7655e0a2aa39dd1f0c6d80d89065b4f34f264d)) + +## [5.6.1](https://github.com/appium/WebDriverAgent/compare/v5.6.0...v5.6.1) (2023-08-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump lint-staged from 13.3.0 to 14.0.0 ([#750](https://github.com/appium/WebDriverAgent/issues/750)) ([0b74bf5](https://github.com/appium/WebDriverAgent/commit/0b74bf5befaa6d87c93a5306beb690a5a0e1843d)) + +## [5.6.0](https://github.com/appium/WebDriverAgent/compare/v5.5.2...v5.6.0) (2023-07-15) + + +### Features + +* apply shouldWaitForQuiescence for activate in /wda/apps/launch ([#739](https://github.com/appium/WebDriverAgent/issues/739)) ([#740](https://github.com/appium/WebDriverAgent/issues/740)) ([66ab695](https://github.com/appium/WebDriverAgent/commit/66ab695f9fa1850145a1d94ef15978b70bc1b032)) + +## [5.5.2](https://github.com/appium/WebDriverAgent/compare/v5.5.1...v5.5.2) (2023-07-07) + + +### Miscellaneous Chores + +* **deps-dev:** bump prettier from 2.8.8 to 3.0.0 ([#735](https://github.com/appium/WebDriverAgent/issues/735)) ([15614d0](https://github.com/appium/WebDriverAgent/commit/15614d030975f2b1eac5919d2353bc015f194d4c)) + +## [5.5.1](https://github.com/appium/WebDriverAgent/compare/v5.5.0...v5.5.1) (2023-06-16) + + +### Bug Fixes + +* Update strongbox API name ([4977032](https://github.com/appium/WebDriverAgent/commit/49770328aeeebacd76011ff1caf13d5b4ed71420)) + +## [5.5.0](https://github.com/appium/WebDriverAgent/compare/v5.4.1...v5.5.0) (2023-06-12) + + +### Features + +* Add accessibility audit extension ([#727](https://github.com/appium/WebDriverAgent/issues/727)) ([78321dd](https://github.com/appium/WebDriverAgent/commit/78321dd3dafdb142eed136b48ec101f1daed50a4)) + +## [5.4.1](https://github.com/appium/WebDriverAgent/compare/v5.4.0...v5.4.1) (2023-06-09) + + +### Bug Fixes + +* Return default testmanagerd version if the info is not available ([#728](https://github.com/appium/WebDriverAgent/issues/728)) ([e6e2dbd](https://github.com/appium/WebDriverAgent/commit/e6e2dbd86fc0c48ae146905f0e69a6223360e856)) + +## [5.4.0](https://github.com/appium/WebDriverAgent/compare/v5.3.3...v5.4.0) (2023-06-09) + + +### Features + +* Drop older screenshoting APIs ([#721](https://github.com/appium/WebDriverAgent/issues/721)) ([4a08d7a](https://github.com/appium/WebDriverAgent/commit/4a08d7a843af6b93b378b4e3dc10f123d2e56359)) + + +### Bug Fixes + +* Streamline errors handling for async block calls ([#725](https://github.com/appium/WebDriverAgent/issues/725)) ([364b779](https://github.com/appium/WebDriverAgent/commit/364b7791393ffae9c048c5cac023e3e7d1813a14)) + +## [5.3.3](https://github.com/appium/WebDriverAgent/compare/v5.3.2...v5.3.3) (2023-06-08) + + +### Miscellaneous Chores + +* Disable automatic screen recording by default ([#726](https://github.com/appium/WebDriverAgent/issues/726)) ([a070223](https://github.com/appium/WebDriverAgent/commit/a070223e0ef43be8dd54d16ee3e3b96603ad5f3a)) + +## [5.3.2](https://github.com/appium/WebDriverAgent/compare/v5.3.1...v5.3.2) (2023-06-07) + + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#723](https://github.com/appium/WebDriverAgent/issues/723)) ([b22f61e](https://github.com/appium/WebDriverAgent/commit/b22f61eda142ee6ec1db8c74a4788e0270ac7740)) + +## [5.3.1](https://github.com/appium/WebDriverAgent/compare/v5.3.0...v5.3.1) (2023-06-06) + + +### Bug Fixes + +* remove Parameter of overriding method should be annotated with __attribute__((noescape)) ([#720](https://github.com/appium/WebDriverAgent/issues/720)) ([5f811ac](https://github.com/appium/WebDriverAgent/commit/5f811ac65ba3ac770e42bd7f8614815df8ec990f)) + +## [5.3.0](https://github.com/appium/WebDriverAgent/compare/v5.2.0...v5.3.0) (2023-05-22) + + +### Features + +* Use strongbox to persist the previous version of the module ([#714](https://github.com/appium/WebDriverAgent/issues/714)) ([4611792](https://github.com/appium/WebDriverAgent/commit/4611792ee5d5d7f39d188b5ebc31017f436c5ace)) + +## [5.2.0](https://github.com/appium/WebDriverAgent/compare/v5.1.6...v5.2.0) (2023-05-20) + + +### Features + +* Replace non-encodable characters in the resulting JSON ([#713](https://github.com/appium/WebDriverAgent/issues/713)) ([cdfae40](https://github.com/appium/WebDriverAgent/commit/cdfae408be0bcf6607f0ca4462925eed2c300f5e)) + +## [5.1.6](https://github.com/appium/WebDriverAgent/compare/v5.1.5...v5.1.6) (2023-05-18) + + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 3.1.11 to 4.0.0 ([#710](https://github.com/appium/WebDriverAgent/issues/710)) ([3e49523](https://github.com/appium/WebDriverAgent/commit/3e495230674a46db29ecea3b36c2ed0ea1bf2842)) + +## [5.1.5](https://github.com/appium/WebDriverAgent/compare/v5.1.4...v5.1.5) (2023-05-18) + + +### Miscellaneous Chores + +* Drop obsolete workarounds for coordinates calculation ([#701](https://github.com/appium/WebDriverAgent/issues/701)) ([259f731](https://github.com/appium/WebDriverAgent/commit/259f7319305b15a3f541957d3ccaa3cb12c9e1a3)) + +## [5.1.4](https://github.com/appium/WebDriverAgent/compare/v5.1.3...v5.1.4) (2023-05-16) + + +### Bug Fixes + +* Prevent freeze on launch/activate of a missing app ([#706](https://github.com/appium/WebDriverAgent/issues/706)) ([c4976e3](https://github.com/appium/WebDriverAgent/commit/c4976e3e99afa4d471bd39c3dccfc7d9f58d8bfc)) + +## [5.1.3](https://github.com/appium/WebDriverAgent/compare/v5.1.2...v5.1.3) (2023-05-16) + + +### Bug Fixes + +* Revert "fix: Assert app is installed before launching or activating it ([#704](https://github.com/appium/WebDriverAgent/issues/704))" ([#705](https://github.com/appium/WebDriverAgent/issues/705)) ([00baeb2](https://github.com/appium/WebDriverAgent/commit/00baeb2045b9aac98d27fe2e96cedce0dde5e8be)) + +## [5.1.2](https://github.com/appium/WebDriverAgent/compare/v5.1.1...v5.1.2) (2023-05-15) + + +### Miscellaneous Chores + +* remove code for old os versions ([#694](https://github.com/appium/WebDriverAgent/issues/694)) ([4a9faa5](https://github.com/appium/WebDriverAgent/commit/4a9faa5f85e0615c18a5f35090335bdbc7d56ebe)) + +## [5.1.1](https://github.com/appium/WebDriverAgent/compare/v5.1.0...v5.1.1) (2023-05-15) + + +### Bug Fixes + +* Assert app is installed before launching or activating it ([#704](https://github.com/appium/WebDriverAgent/issues/704)) ([94e5c51](https://github.com/appium/WebDriverAgent/commit/94e5c51bce1d4518418e999b4ac466cd46ca3bc3)) + +## [5.1.0](https://github.com/appium/WebDriverAgent/compare/v5.0.0...v5.1.0) (2023-05-14) + + +### Features + +* Add a possibility to provide a target picker value ([#699](https://github.com/appium/WebDriverAgent/issues/699)) ([fc76aee](https://github.com/appium/WebDriverAgent/commit/fc76aeecb087429974b7b52b725173186e6f0246)) + + +### Code Refactoring + +* Remove obsolete coordinate calculation workarounds needed for older xCode SDKs ([#698](https://github.com/appium/WebDriverAgent/issues/698)) ([025b42c](https://github.com/appium/WebDriverAgent/commit/025b42c8a34ff0beba4379f4cb0c1d79d2b222ed)) + +## [5.0.0](https://github.com/appium/WebDriverAgent/compare/v4.15.1...v5.0.0) (2023-05-14) + + +### ⚠ BREAKING CHANGES + +* The minimum supported xCode/iOS version is now 13/15.0 + +### Code Refactoring + +* Drop workarounds for legacy iOS versions ([#696](https://github.com/appium/WebDriverAgent/issues/696)) ([bb562b9](https://github.com/appium/WebDriverAgent/commit/bb562b96db6aad476970ef7bd352cb8df4f1e6c2)) + +## [4.15.1](https://github.com/appium/WebDriverAgent/compare/v4.15.0...v4.15.1) (2023-05-06) + + +### Performance Improvements + +* tune webDriverAgentUrl case by skiping xcodebuild stuff ([#691](https://github.com/appium/WebDriverAgent/issues/691)) ([d8f1457](https://github.com/appium/WebDriverAgent/commit/d8f1457b591b2dd00040f8336c1a7a728af871d2)) + +## [4.15.0](https://github.com/appium/WebDriverAgent/compare/v4.14.0...v4.15.0) (2023-05-04) + + +### Features + +* Make isFocused attribute available for iOS elements ([#692](https://github.com/appium/WebDriverAgent/issues/692)) ([0ec74ce](https://github.com/appium/WebDriverAgent/commit/0ec74ce32c817a5884228ccb2ec31f0a5a4de9c3)) + +## [4.14.0](https://github.com/appium/WebDriverAgent/compare/v4.13.2...v4.14.0) (2023-05-02) + + +### Features + +* start wda process via Xctest in a real device ([#687](https://github.com/appium/WebDriverAgent/issues/687)) ([e1c0f83](https://github.com/appium/WebDriverAgent/commit/e1c0f836a68ad2efbedfc77343794d0d97ef6090)) + +## [4.13.2](https://github.com/appium/WebDriverAgent/compare/v4.13.1...v4.13.2) (2023-04-28) + + +### Miscellaneous Chores + +* add withoutSession for pasteboard for debug ([#688](https://github.com/appium/WebDriverAgent/issues/688)) ([edcbf9e](https://github.com/appium/WebDriverAgent/commit/edcbf9e6af903c6f490ca90ff915497ad53bb8b5)) + +## [4.13.1](https://github.com/appium/WebDriverAgent/compare/v4.13.0...v4.13.1) (2023-04-04) + + +### Bug Fixes + +* Fixed Xpath lookup for Xcode 14.3 ([#681](https://github.com/appium/WebDriverAgent/issues/681)) ([3e0b191](https://github.com/appium/WebDriverAgent/commit/3e0b1914f87585ed69ba20d960502eabb058941c)) + +## [4.13.0](https://github.com/appium/WebDriverAgent/compare/v4.12.2...v4.13.0) (2023-02-23) + + +### Features + +* Increase Xpath Lookup Performance ([#666](https://github.com/appium/WebDriverAgent/issues/666)) ([1696f4b](https://github.com/appium/WebDriverAgent/commit/1696f4bb879152ef04408940849708654072c797)) + +## [4.12.2](https://github.com/appium/WebDriverAgent/compare/v4.12.1...v4.12.2) (2023-02-22) + + +### Miscellaneous Chores + +* Make sure the test is never going to be unexpectedly interrupted ([#664](https://github.com/appium/WebDriverAgent/issues/664)) ([cafe47e](https://github.com/appium/WebDriverAgent/commit/cafe47e9bea9649a0e9b4a2b96ca44434bbac411)) + +## [4.12.1](https://github.com/appium/WebDriverAgent/compare/v4.12.0...v4.12.1) (2023-02-20) + + +### Bug Fixes + +* Return null if no simulated location has been previously set ([#663](https://github.com/appium/WebDriverAgent/issues/663)) ([6a5c48b](https://github.com/appium/WebDriverAgent/commit/6a5c48bd2ffc43c0f0d9bf781671bbcf171f9375)) + +## [4.12.0](https://github.com/appium/WebDriverAgent/compare/v4.11.0...v4.12.0) (2023-02-20) + + +### Features + +* Add support of the simulated geolocation setting ([#662](https://github.com/appium/WebDriverAgent/issues/662)) ([ebb9e60](https://github.com/appium/WebDriverAgent/commit/ebb9e60d56c0e0db9f509437ed639a3a39f6011b)) + +## [4.11.0](https://github.com/appium/WebDriverAgent/compare/v4.10.24...v4.11.0) (2023-02-19) + + +### Features + +* Add openUrl handler available since Xcode 14.3 ([#661](https://github.com/appium/WebDriverAgent/issues/661)) ([bee564e](https://github.com/appium/WebDriverAgent/commit/bee564e8c6b975aff07fd1244583f0727a0f5470)) + +## [4.10.24](https://github.com/appium/WebDriverAgent/compare/v4.10.23...v4.10.24) (2023-02-17) + + +### Bug Fixes + +* Catch unexpected exceptions thrown by the alerts monitor ([#660](https://github.com/appium/WebDriverAgent/issues/660)) ([aa22555](https://github.com/appium/WebDriverAgent/commit/aa22555f0dcf98de43c95cb20be73e911a97741e)) + +## [4.10.23](https://github.com/appium/WebDriverAgent/compare/v4.10.22...v4.10.23) (2023-02-05) + + +### Miscellaneous Chores + +* bundle:tv for tvOS ([#657](https://github.com/appium/WebDriverAgent/issues/657)) ([9d2d047](https://github.com/appium/WebDriverAgent/commit/9d2d047fba57a33787c66a1e8a8449b9538c67be)) + +## [4.10.22](https://github.com/appium/WebDriverAgent/compare/v4.10.21...v4.10.22) (2023-01-30) + + +### Bug Fixes + +* Pull defaultAdditionalRequestParameters dynamically ([#658](https://github.com/appium/WebDriverAgent/issues/658)) ([d7c397b](https://github.com/appium/WebDriverAgent/commit/d7c397b0260a71568edd6d99ecf7b39ca3503083)) + +## [4.10.21](https://github.com/appium/WebDriverAgent/compare/v4.10.20...v4.10.21) (2023-01-26) + + +### Bug Fixes + +* Properly update maxDepth while fetching snapshots ([#655](https://github.com/appium/WebDriverAgent/issues/655)) ([6f99bab](https://github.com/appium/WebDriverAgent/commit/6f99bab5fbdbf65c9ef74c42b5f1b4c658aeaafb)) + +## [4.10.20](https://github.com/appium/WebDriverAgent/compare/v4.10.19...v4.10.20) (2023-01-17) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 19.0.5 to 20.0.2 ([#651](https://github.com/appium/WebDriverAgent/issues/651)) ([e96c367](https://github.com/appium/WebDriverAgent/commit/e96c367cb0d9461bb5e443740504969a4cb857e1)) + +## [4.10.19](https://github.com/appium/WebDriverAgent/compare/v4.10.18...v4.10.19) (2023-01-13) + + +### Miscellaneous Chores + +* **deps-dev:** bump appium-xcode from 4.0.5 to 5.0.0 ([#652](https://github.com/appium/WebDriverAgent/issues/652)) ([75c247f](https://github.com/appium/WebDriverAgent/commit/75c247fe82ebe7b2b8ba0d79528cadeda871e229)) + +## [4.10.18](https://github.com/appium/WebDriverAgent/compare/v4.10.17...v4.10.18) (2022-12-30) + + +### Miscellaneous Chores + +* simplify Script/build-webdriveragent.js ([#647](https://github.com/appium/WebDriverAgent/issues/647)) ([81dab6c](https://github.com/appium/WebDriverAgent/commit/81dab6ca0645f9925a8515abfb4851d6e85da7e9)) + +## [4.10.17](https://github.com/appium/WebDriverAgent/compare/v4.10.16...v4.10.17) (2022-12-30) + + +### Miscellaneous Chores + +* add ARCHS=arm64 for a release package build ([#649](https://github.com/appium/WebDriverAgent/issues/649)) ([08612aa](https://github.com/appium/WebDriverAgent/commit/08612aade1833c384914bb618675b5653d5f5118)) + +## [4.10.16](https://github.com/appium/WebDriverAgent/compare/v4.10.15...v4.10.16) (2022-12-29) + + +### Miscellaneous Chores + +* build only arm64 for generic build in a release ([#648](https://github.com/appium/WebDriverAgent/issues/648)) ([63e175d](https://github.com/appium/WebDriverAgent/commit/63e175d56526d9fb74d9053dbe60fd0c80b9c670)) + +## [4.10.15](https://github.com/appium/WebDriverAgent/compare/v4.10.14...v4.10.15) (2022-12-16) + + +### Miscellaneous Chores + +* **deps:** bump @appium/base-driver from 8.7.3 to 9.0.0 ([#645](https://github.com/appium/WebDriverAgent/issues/645)) ([35dd981](https://github.com/appium/WebDriverAgent/commit/35dd98111f1d8222bc0cb412c11cb1442d10295e)) +* **deps:** bump appium-ios-simulator from 4.2.1 to 5.0.1 ([#646](https://github.com/appium/WebDriverAgent/issues/646)) ([7911cbb](https://github.com/appium/WebDriverAgent/commit/7911cbb3607b1d75091bdf3dc436baae3868854a)) + +## [4.10.14](https://github.com/appium/WebDriverAgent/compare/v4.10.13...v4.10.14) (2022-12-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump @appium/test-support from 2.0.2 to 3.0.0 ([#644](https://github.com/appium/WebDriverAgent/issues/644)) ([ab84580](https://github.com/appium/WebDriverAgent/commit/ab8458027457563b7faaeef36d9019b7ac1921b0)) +* **deps:** bump @appium/support from 2.61.1 to 3.0.0 ([#643](https://github.com/appium/WebDriverAgent/issues/643)) ([3ca197a](https://github.com/appium/WebDriverAgent/commit/3ca197ac7526036e408584207b26129847a615ca)) + +## [4.10.13](https://github.com/appium/WebDriverAgent/compare/v4.10.12...v4.10.13) (2022-12-13) + +## [4.10.12](https://github.com/appium/WebDriverAgent/compare/v4.10.11...v4.10.12) (2022-12-08) + + +### Bug Fixes + +* Provide proper xcodebuild argument for tvOS ([#640](https://github.com/appium/WebDriverAgent/issues/640)) ([72bd327](https://github.com/appium/WebDriverAgent/commit/72bd32780f26ae0f60b30e0cee8fc585aea600fe)) + +## [4.10.11](https://github.com/appium/WebDriverAgent/compare/v4.10.10...v4.10.11) (2022-11-29) + +## [4.10.10](https://github.com/appium/WebDriverAgent/compare/v4.10.9...v4.10.10) (2022-11-25) + + +### Bug Fixes + +* Only check existence if firstMatch is applied ([#638](https://github.com/appium/WebDriverAgent/issues/638)) ([5394fe8](https://github.com/appium/WebDriverAgent/commit/5394fe8cc2eda3d1668685bd00f9f7383e122627)) + +## [4.10.9](https://github.com/appium/WebDriverAgent/compare/v4.10.8...v4.10.9) (2022-11-25) + +## [4.10.8](https://github.com/appium/WebDriverAgent/compare/v4.10.7...v4.10.8) (2022-11-24) + +## [4.10.7](https://github.com/appium/WebDriverAgent/compare/v4.10.6...v4.10.7) (2022-11-24) + +## [4.10.6](https://github.com/appium/WebDriverAgent/compare/v4.10.5...v4.10.6) (2022-11-23) + +## [4.10.5](https://github.com/appium/WebDriverAgent/compare/v4.10.4...v4.10.5) (2022-11-22) + +## [4.10.4](https://github.com/appium/WebDriverAgent/compare/v4.10.3...v4.10.4) (2022-11-22) + +## [4.10.3](https://github.com/appium/WebDriverAgent/compare/v4.10.2...v4.10.3) (2022-11-22) + +## [4.10.2](https://github.com/appium/WebDriverAgent/compare/v4.10.1...v4.10.2) (2022-11-06) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8c5f46e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to WebDriverAgent +We want to make contributing to this project as easy and transparent as +possible. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe +disclosure of security bugs. In those cases, please go through the process +outlined on that page and do not file a public issue. + +## Coding Style +* 2 spaces for indentation rather than tabs +* 80 character line length + +## License +By contributing to WebDriverAgent, you agree that your contributions will be licensed +under its [BSD license](LICENSE). diff --git a/Configurations/IOSSettings.xcconfig b/Configurations/IOSSettings.xcconfig new file mode 100644 index 0000000..3bde5a2 --- /dev/null +++ b/Configurations/IOSSettings.xcconfig @@ -0,0 +1,31 @@ +EXCLUDED_ARCHS = i386 + +GCC_TREAT_WARNINGS_AS_ERRORS = YES +GCC_WARN_PEDANTIC = YES +GCC_WARN_SHADOW = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_SIGN_COMPARE = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +GCC_WARN_UNUSED_PARAMETER = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES + +CLANG_ANALYZER_NONNULL = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_ASSIGN_ENUM = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES + +RUN_CLANG_STATIC_ANALYZER = YES + +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) + +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/Configurations/IOSTestSettings.xcconfig b/Configurations/IOSTestSettings.xcconfig new file mode 100644 index 0000000..17824cc --- /dev/null +++ b/Configurations/IOSTestSettings.xcconfig @@ -0,0 +1 @@ +EXCLUDED_ARCHS = i386 diff --git a/Configurations/TVOSSettings.xcconfig b/Configurations/TVOSSettings.xcconfig new file mode 100644 index 0000000..3bde5a2 --- /dev/null +++ b/Configurations/TVOSSettings.xcconfig @@ -0,0 +1,31 @@ +EXCLUDED_ARCHS = i386 + +GCC_TREAT_WARNINGS_AS_ERRORS = YES +GCC_WARN_PEDANTIC = YES +GCC_WARN_SHADOW = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_SIGN_COMPARE = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +GCC_WARN_UNUSED_PARAMETER = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES + +CLANG_ANALYZER_NONNULL = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_ASSIGN_ENUM = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES + +RUN_CLANG_STATIC_ANALYZER = YES + +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) + +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/Configurations/TVOSTestSettings.xcconfig b/Configurations/TVOSTestSettings.xcconfig new file mode 100644 index 0000000..17824cc --- /dev/null +++ b/Configurations/TVOSTestSettings.xcconfig @@ -0,0 +1 @@ +EXCLUDED_ARCHS = i386 diff --git a/Fastlane/Fastfile b/Fastlane/Fastfile new file mode 100644 index 0000000..6e657a7 --- /dev/null +++ b/Fastlane/Fastfile @@ -0,0 +1,13 @@ +XC_PROJECT = File.absolute_path('../WebDriverAgent.xcodeproj') + +lane :test do + # https://docs.fastlane.tools/actions/scan/ + run_tests( + project: XC_PROJECT, + fail_build: true, + scheme: ENV['SCHEME'], + sdk: ENV['SDK'], + destination: ENV['DEST'], + number_of_retries: 3 + ) +end diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..90c9356 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane", '2.217.0' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b3db8e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For WebDriverAgent software + +Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PrivateHeaders/AccessibilityUtilities/AXSettings.h b/PrivateHeaders/AccessibilityUtilities/AXSettings.h new file mode 100644 index 0000000..5964d84 --- /dev/null +++ b/PrivateHeaders/AccessibilityUtilities/AXSettings.h @@ -0,0 +1,15 @@ + +/* Generated by RuntimeBrowser + Image: /System/Library/PrivateFrameworks/AccessibilityUtilities.framework/AccessibilityUtilities + */ + +@interface AXSettings : NSObject + +@property bool reduceMotionEnabled; + ++ (id)sharedInstance; + +- (void)setReduceMotionEnabled:(bool)arg1; +- (bool)reduceMotionEnabled; + +@end diff --git a/PrivateHeaders/MobileCoreServices/LSApplicationWorkspace.h b/PrivateHeaders/MobileCoreServices/LSApplicationWorkspace.h new file mode 100644 index 0000000..736ab8b --- /dev/null +++ b/PrivateHeaders/MobileCoreServices/LSApplicationWorkspace.h @@ -0,0 +1,127 @@ + +/* Generated by RuntimeBrowser + Image: /System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices + */ + +@interface LSApplicationWorkspace : NSObject { + NSXPCConnection * _connection; + NSMutableDictionary * _createdInstallProgresses; +} + +@property (readonly) NSXPCConnection *connection; +@property (readonly) NSMutableDictionary *createdInstallProgresses; + ++ (id)activeManagedConfigurationRestrictionUUIDs; ++ (id)callbackQueue; ++ (instancetype)defaultWorkspace; + +- (id)URLOverrideForURL:(id)arg1; +- (id)URLSchemesOfType:(long long)arg1; +- (void)_LSClearSchemaCaches; +- (void)_LSFailedToOpenURL:(id)arg1 withBundle:(id)arg2; +- (bool)_LSPrivateDatabaseNeedsRebuild; +- (bool)_LSPrivateRebuildApplicationDatabasesForSystemApps:(bool)arg1 internal:(bool)arg2 user:(bool)arg3; +- (void)_LSPrivateSyncWithMobileInstallation; +- (void)addObserver:(id)arg1; +- (id)allApplications; +- (id)allInstalledApplications; +- (id)applicationForOpeningResource:(id)arg1; +- (id)applicationForUserActivityDomainName:(id)arg1; +- (id)applicationForUserActivityType:(id)arg1; +- (bool)applicationIsInstalled:(id)arg1; +- (id)applicationProxiesWithPlistFlags:(unsigned int)arg1 bundleFlags:(unsigned long long)arg2; +- (id)applicationsAvailableForHandlingURLScheme:(id)arg1; +- (id)applicationsAvailableForOpeningDocument:(id)arg1; +- (id)applicationsAvailableForOpeningURL:(id)arg1; +- (id)applicationsAvailableForOpeningURL:(id)arg1 legacySPI:(bool)arg2; +- (id)applicationsForUserActivityType:(id)arg1; +- (id)applicationsForUserActivityType:(id)arg1 limit:(unsigned long long)arg2; +- (id)applicationsOfType:(unsigned long long)arg1; +- (id)applicationsWithAudioComponents; +- (id)applicationsWithUIBackgroundModes; +- (id)applicationsWithVPNPlugins; +- (id)bundleIdentifiersForMachOUUIDs:(id)arg1 error:(id*)arg2; +- (void)clearAdvertisingIdentifier; +- (void)clearCreatedProgressForBundleID:(id)arg1; +- (id)connection; +- (id)createdInstallProgresses; +- (void)dealloc; +- (id)delegateProxy; +- (id)deviceIdentifierForAdvertising; +- (id)deviceIdentifierForVendor; +- (id)directionsApplications; +- (bool)downgradeApplicationToPlaceholder:(id)arg1 withOptions:(id)arg2 error:(id*)arg3; +- (void)enumerateApplicationsForSiriWithBlock:(id /* block */)arg1; +- (void)enumerateApplicationsOfType:(unsigned long long)arg1 block:(id /* block */)arg2; +- (void)enumerateApplicationsOfType:(unsigned long long)arg1 legacySPI:(bool)arg2 block:(id /* block */)arg3; +- (void)enumerateBundlesOfType:(unsigned long long)arg1 block:(id /* block */)arg2; +- (void)enumerateBundlesOfType:(unsigned long long)arg1 legacySPI:(bool)arg2 block:(id /* block */)arg3; +- (void)enumerateBundlesOfType:(unsigned long long)arg1 usingBlock:(id /* block */)arg2; +- (void)enumeratePluginsMatchingQuery:(id)arg1 withBlock:(id /* block */)arg2; +- (bool)establishConnection; +- (bool)getClaimedActivityTypes:(id*)arg1 domains:(id*)arg2; +- (unsigned long long)getInstallTypeForOptions:(id)arg1 andApp:(id)arg2; +- (void)getKnowledgeUUID:(id*)arg1 andSequenceNumber:(id*)arg2; +- (bool)installApplication:(id)arg1 withOptions:(id)arg2; +- (bool)installApplication:(id)arg1 withOptions:(id)arg2 error:(id*)arg3; +- (bool)installApplication:(id)arg1 withOptions:(id)arg2 error:(id*)arg3 usingBlock:(id /* block */)arg4; +- (id)installBundle:(id)arg1 withOptions:(id)arg2 usingBlock:(id /* block */)arg3 forApp:(id)arg4 withError:(id*)arg5 outInstallProgress:(id*)arg6; +- (bool)installPhaseFinishedForProgress:(id)arg1; +- (id)installProgressForApplication:(id)arg1 withPhase:(unsigned long long)arg2; +- (id)installProgressForBundleID:(id)arg1 makeSynchronous:(unsigned char)arg2; +- (id)installedPlugins; +- (bool)invalidateIconCache:(id)arg1; +- (bool)isApplicationAvailableToOpenURL:(id)arg1 error:(id*)arg2; +- (bool)isApplicationAvailableToOpenURL:(id)arg1 includePrivateURLSchemes:(bool)arg2 error:(id*)arg3; +- (bool)isApplicationAvailableToOpenURLCommon:(id)arg1 includePrivateURLSchemes:(bool)arg2 error:(id*)arg3; +- (id)legacyApplicationProxiesListWithType:(unsigned long long)arg1; +- (id)machOUUIDsForBundleIdentifiers:(id)arg1 error:(id*)arg2; +- (id)observedInstallProgresses; +- (bool)openApplicationWithBundleID:(id)arg1; +- (bool)openSensitiveURL:(id)arg1 withOptions:(id)arg2; +- (bool)openSensitiveURL:(id)arg1 withOptions:(id)arg2 error:(id*)arg3; +- (bool)openURL:(id)arg1; +- (bool)openURL:(id)arg1 withOptions:(id)arg2; +- (bool)openURL:(id)arg1 withOptions:(id)arg2 error:(id*)arg3; +- (void)openUserActivity:(id)arg1 withApplicationProxy:(id)arg2 completionHandler:(id /* block */)arg3; +- (void)openUserActivity:(id)arg1 withApplicationProxy:(id)arg2 options:(id)arg3 completionHandler:(id /* block */)arg4; +- (id)operationToOpenResource:(id)arg1 usingApplication:(id)arg2 uniqueDocumentIdentifier:(id)arg3 sourceIsManaged:(bool)arg4 userInfo:(id)arg5 delegate:(id)arg6; +- (id)operationToOpenResource:(id)arg1 usingApplication:(id)arg2 uniqueDocumentIdentifier:(id)arg3 userInfo:(id)arg4; +- (id)operationToOpenResource:(id)arg1 usingApplication:(id)arg2 uniqueDocumentIdentifier:(id)arg3 userInfo:(id)arg4 delegate:(id)arg5; +- (id)operationToOpenResource:(id)arg1 usingApplication:(id)arg2 userInfo:(id)arg3; +- (id)placeholderApplications; +- (id)pluginsMatchingQuery:(id)arg1 applyFilter:(id /* block */)arg2; +- (id)pluginsWithIdentifiers:(id)arg1 protocols:(id)arg2 version:(id)arg3; +- (id)pluginsWithIdentifiers:(id)arg1 protocols:(id)arg2 version:(id)arg3 applyFilter:(id /* block */)arg4; +- (id)pluginsWithIdentifiers:(id)arg1 protocols:(id)arg2 version:(id)arg3 withFilter:(id /* block */)arg4; +- (id)privateURLSchemes; +- (id)publicURLSchemes; +- (bool)registerApplication:(id)arg1; +- (bool)registerApplicationDictionary:(id)arg1; +- (bool)registerApplicationDictionary:(id)arg1 withObserverNotification:(int)arg2; +- (bool)registerBundleWithInfo:(id)arg1 options:(id)arg2 type:(unsigned long long)arg3 progress:(id)arg4; +- (bool)registerPlugin:(id)arg1; +- (id)remoteObserver; +- (void)removeInstallProgressForBundleID:(id)arg1; +- (void)removeObserver:(id)arg1; +- (id)removedSystemApplications; +- (bool)restoreSystemApplication:(id)arg1; +- (void)scanForApplicationStateChangesFromRank:(id)arg1 toRank:(id)arg2; +- (void)scanForApplicationStateChangesFromWhitelist:(id)arg1 to:(id)arg2; +- (void)sendApplicationStateChangedNotificationsFor:(id)arg1; +- (void)sendInstallNotificationForApp:(id)arg1 withPlugins:(id)arg2; +- (void)sendUninstallNotificationForApp:(id)arg1 withPlugins:(id)arg2; +- (bool)uninstallApplication:(id)arg1 withOptions:(id)arg2; +- (bool)uninstallApplication:(id)arg1 withOptions:(id)arg2 error:(id*)arg3 usingBlock:(id /* block */)arg4; +- (bool)uninstallApplication:(id)arg1 withOptions:(id)arg2 usingBlock:(id /* block */)arg3; +- (bool)uninstallSystemApplication:(id)arg1 withOptions:(id)arg2 usingBlock:(id /* block */)arg3; +- (bool)unregisterApplication:(id)arg1; +- (bool)unregisterPlugin:(id)arg1; +- (id)unrestrictedApplications; +- (bool)updateRecordForApp:(id)arg1 withSINF:(id)arg2 iTunesMetadata:(id)arg3 error:(id*)arg4; +- (bool)updateSINFWithData:(id)arg1 forApplication:(id)arg2 options:(id)arg3 error:(id*)arg4; +- (bool)updateiTunesMetadataWithData:(id)arg1 forApplication:(id)arg2 options:(id)arg3 error:(id*)arg4; + +- (void)_sf_openURL:(id)arg1 withOptions:(id)arg2 completionHandler:(id /* block */)arg3; + +@end diff --git a/PrivateHeaders/TextInput/TIPreferencesController.h b/PrivateHeaders/TextInput/TIPreferencesController.h new file mode 100644 index 0000000..df30f74 --- /dev/null +++ b/PrivateHeaders/TextInput/TIPreferencesController.h @@ -0,0 +1,45 @@ +/** + * iOS-Runtime-Headers/PrivateFrameworks/TextInput.framework. + * Text Input preferences controller to modify the keyboard preferences for iOS 8+. + * + * Note: + * "autocorrection" will be PrivateFrameworks/TextInput.framework/TIKeyboardState.h in the future? + */ +@interface TIPreferencesController : NSObject + +/** + * Whether the autocorrection is enabled. + */ +@property BOOL autocorrectionEnabled; + +/** + * Whether the predication is enabled. + * */ +@property BOOL predictionEnabled; + +/** + The shared singleton instance. + */ ++ (instancetype)sharedPreferencesController; + +/** + Synchronise the change to save it on disk. + */ +- (void)synchronizePreferences; + +/** + * Modify the preference @c value by the @c key + * + * @param value The value to set it to @c key + * @param key The key name to set @c value to + */ +- (void)setValue:(NSValue *)value forPreferenceKey:(NSString *)key; + +/** + * Get the preferenve by @c key + * + * @param key The key name to get the value + * @return Whether the @c key is enabled + */ +- (BOOL)boolForPreferenceKey:(NSString *)key; +@end diff --git a/PrivateHeaders/UIKitCore/UIKeyboardImpl.h b/PrivateHeaders/UIKitCore/UIKeyboardImpl.h new file mode 100644 index 0000000..bc89cef --- /dev/null +++ b/PrivateHeaders/UIKitCore/UIKeyboardImpl.h @@ -0,0 +1,27 @@ +#if TARGET_OS_SIMULATOR +/** + * iOS-Runtime-Headers/PrivateFrameworks/UIKitCore.framework/UIKeyboardImpl.h + */ +@interface UIKeyboardImpl ++ (instancetype)sharedInstance; +/** + * Modify software keyboard condition on simulators for over Xcode 6 + * This setting is global. The change applies to all instances of UIKeyboardImpl. + * + * Idea: https://chromium.googlesource.com/chromium/src/base/+/ababb4cf8b6049a642a2f361b1006a07561c2d96/test/test_support_ios.mm#41 + * + * @param enabled Whether turn setAutomaticMinimizationEnabled on + */ +- (void)setAutomaticMinimizationEnabled:(BOOL)enabled; + +/** +* Modify software keyboard condition on simulators for over Xcode 6 +* This setting is global. The change applies to all instances of UIKeyboardImpl. +* +* Idea: https://chromium.googlesource.com/chromium/src/base/+/ababb4cf8b6049a642a2f361b1006a07561c2d96/test/test_support_ios.mm#41 +* +* @param enabled Whether turn setSoftwareKeyboardShownByTouch on +*/ +- (void)setSoftwareKeyboardShownByTouch:(BOOL)enabled; +@end +#endif // TARGET_IPHONE_SIMULATOR diff --git a/PrivateHeaders/XCTest/CDStructures.h b/PrivateHeaders/XCTest/CDStructures.h new file mode 100644 index 0000000..eaf3f46 --- /dev/null +++ b/PrivateHeaders/XCTest/CDStructures.h @@ -0,0 +1,28 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#pragma mark Blocks + +typedef void (^CDUnknownBlockType)(void); // return type and parameters are unknown + +typedef struct { + unsigned int _field1; + unsigned int _field2; + unsigned int _field3; + unsigned int _field4; + unsigned int _field5; + unsigned int _field6; + unsigned int _field7; +} CDStruct_a561fd19; + +typedef struct { + unsigned short _field1; + unsigned short _field2; + unsigned short _field3[1]; +} CDStruct_27a325c0; + +int _XCTSetApplicationStateTimeout(double timeout); +double _XCTApplicationStateTimeout(void); diff --git a/PrivateHeaders/XCTest/NSString-XCTAdditions.h b/PrivateHeaders/XCTest/NSString-XCTAdditions.h new file mode 100644 index 0000000..8c613fd --- /dev/null +++ b/PrivateHeaders/XCTest/NSString-XCTAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface NSString (XCTAdditions) +- (id)xct_quotedSwiftStringRepresentation; +@end diff --git a/PrivateHeaders/XCTest/NSValue-XCTestAdditions.h b/PrivateHeaders/XCTest/NSValue-XCTestAdditions.h new file mode 100644 index 0000000..b33d3e0 --- /dev/null +++ b/PrivateHeaders/XCTest/NSValue-XCTestAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface NSValue (XCTestAdditions) +- (id)xct_contentDescription; +@end diff --git a/PrivateHeaders/XCTest/UIGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UIGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..0dd52d4 --- /dev/null +++ b/PrivateHeaders/XCTest/UIGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UIGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/UILongPressGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UILongPressGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..dae196e --- /dev/null +++ b/PrivateHeaders/XCTest/UILongPressGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UILongPressGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/UIPanGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UIPanGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..9ae8c05 --- /dev/null +++ b/PrivateHeaders/XCTest/UIPanGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UIPanGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/UIPinchGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UIPinchGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..8f25457 --- /dev/null +++ b/PrivateHeaders/XCTest/UIPinchGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UIPinchGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/UISwipeGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UISwipeGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..5ce8016 --- /dev/null +++ b/PrivateHeaders/XCTest/UISwipeGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UISwipeGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/UITapGestureRecognizer-RecordingAdditions.h b/PrivateHeaders/XCTest/UITapGestureRecognizer-RecordingAdditions.h new file mode 100644 index 0000000..7a3b04f --- /dev/null +++ b/PrivateHeaders/XCTest/UITapGestureRecognizer-RecordingAdditions.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface UITapGestureRecognizer (RecordingAdditions) +- (id)_automationName; +@end diff --git a/PrivateHeaders/XCTest/XCAXClient_iOS.h b/PrivateHeaders/XCTest/XCAXClient_iOS.h new file mode 100644 index 0000000..fcdd197 --- /dev/null +++ b/PrivateHeaders/XCTest/XCAXClient_iOS.h @@ -0,0 +1,55 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import +#import + +@class NSMutableDictionary; + +@interface XCAXClient_iOS : NSObject +{ + NSMutableDictionary *_userTestingNotificationHandlers; + NSMutableDictionary *_cacheAccessibilityLoadedValuesForPIDs; + unsigned long long *_alertNotificationCounter; +} +@property double AXTimeout; + +// Added since Xcode 10.2 +@property(readonly) id applicationProcessTracker; + +- (BOOL)_setAXTimeout:(double)arg1 error:(NSError **)arg2; +- (NSData *)screenshotData; +- (BOOL)performAction:(int)arg1 onElement:(id)arg2 value:(id)arg3 error:(id *)arg4; +- (id)parameterizedAttributeForElement:(id)arg1 attribute:(id)arg2 parameter:(id)arg3; +- (BOOL)setAttribute:(id)arg1 value:(id)arg2 element:(id)arg3 outError:(id *)arg4; +// since Xcode10 +- (id)attributesForElement:(id)arg1 attributes:(id)arg2 error:(id *)arg3; +- (id)attributesForElementSnapshot:(id)arg1 attributeList:(id)arg2; +- (id)snapshotForApplication:(id)arg1 attributeList:(id)arg2 parameters:(id)arg3; +- (id)defaultParameters; +- (id)defaultAttributes; +- (void)notifyWhenViewControllerViewDidDisappearReply:(CDUnknownBlockType)arg1; +- (void)notifyWhenViewControllerViewDidAppearReply:(CDUnknownBlockType)arg1; +- (void)notifyWhenNoAnimationsAreActiveForApplication:(id)arg1 reply:(CDUnknownBlockType)arg2; +- (void)notifyWhenEventLoopIsIdleForApplication:(id)arg1 reply:(CDUnknownBlockType)arg2; +- (id)interruptingUIElementAffectingSnapshot:(id)arg1; +- (void)handleAccessibilityNotification:(int)arg1 withPayload:(id)arg2; +- (void)notifyOnNextOccurrenceOfUserTestingEvent:(id)arg1 handler:(CDUnknownBlockType)arg2; +- (void)handleUserTestingNotification:(id)arg1; +- (id)elementAtPoint:(CGPoint)arg1 error:(id *)arg2; +- (BOOL)cachedAccessibilityLoadedValueForPID:(int)arg1; +- (NSArray/*XCAccessibilityElement*/ *)activeApplications; +- (id)systemApplication; +- (BOOL)enableFauxCollectionViewCells:(id *)arg1; +- (BOOL)loadAccessibility:(id *)arg1; +- (BOOL)_registerForAXNotification:(int)arg1 error:(id *)arg2; +- (BOOL)_loadAccessibility:(id *)arg1; +// Since Xcode 11 +- (id)requestSnapshotForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(id)arg2 parameters:(id)arg3 error:(NSError **)arg4; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCActivityRecord.h b/PrivateHeaders/XCTest/XCActivityRecord.h new file mode 100644 index 0000000..0a74cdf --- /dev/null +++ b/PrivateHeaders/XCTest/XCActivityRecord.h @@ -0,0 +1,39 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSData, NSDate, NSString, NSUUID, XCSynthesizedEventRecord; + +@interface XCActivityRecord : NSObject +{ + NSString *_title; + NSUUID *_uuid; + NSDate *_start; + NSDate *_finish; + BOOL _hasSubactivities; + NSData *_screenImageData; + id/*XCElementSnapshot*/ _snapshot; + NSArray *_elementsOfInterest; + XCSynthesizedEventRecord *_synthesizedEvent; + NSData *_diagnosticReportData; + NSData *_memoryGraphData; +} + +@property(copy) NSData *memoryGraphData; // @synthesize memoryGraphData=_memoryGraphData; +@property(copy) NSData *diagnosticReportData; // @synthesize diagnosticReportData=_diagnosticReportData; +@property(retain) XCSynthesizedEventRecord *synthesizedEvent; // @synthesize synthesizedEvent=_synthesizedEvent; +@property(copy) NSArray *elementsOfInterest; // @synthesize elementsOfInterest=_elementsOfInterest; +@property(retain) id/*XCElementSnapshot*/ *snapshot; // @synthesize snapshot=_snapshot; +@property(copy) NSData *screenImageData; // @synthesize screenImageData=_screenImageData; +@property BOOL hasSubactivities; // @synthesize hasSubactivities=_hasSubactivities; +@property(copy) NSDate *start; // @synthesize start=_start; +@property(copy) NSDate *finish; // @synthesize finish=_finish; +@property(copy) NSUUID *uuid; // @synthesize uuid=_uuid; +@property(copy) NSString *title; // @synthesize title=_title; +@property(readonly) double duration; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCApplicationMonitor.h b/PrivateHeaders/XCTest/XCApplicationMonitor.h new file mode 100644 index 0000000..6a4386d --- /dev/null +++ b/PrivateHeaders/XCTest/XCApplicationMonitor.h @@ -0,0 +1,44 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "XCTestObservation.h" + +@class NSArray, NSMutableDictionary, NSObject, NSString; + +@interface XCApplicationMonitor : NSObject +{ + NSMutableDictionary *_applicationImplementations; + NSMutableDictionary *_applicationProcessesForPID; + NSMutableDictionary *_applicationProcessesForToken; + NSMutableSet *_launchedApplications; + NSObject *_queue; +} +@property NSObject *queue; // @synthesize queue=_queue; +@property(readonly, copy) NSArray *monitoredApplications; + ++ (instancetype)sharedMonitor; +- (void)requestAutomationSessionForTestTargetWithPID:(int)arg1 reply:(CDUnknownBlockType)arg2; +- (void)processWithToken:(id)arg1 exitedWithStatus:(int)arg2; +- (void)stopTrackingProcessWithToken:(id)arg1; +- (void)crashInProcessWithBundleID:(id)arg1 path:(id)arg2 pid:(int)arg3 symbol:(id)arg4; +- (void)waitForUnrequestedTerminationOfLaunchedApplicationsWithTimeout:(double)arg1; +- (void)_waitForCrashReportOrCleanExitStatusOfApp:(id)arg1; +- (id)_appFromSet:(id)arg1 thatTransitionedToNotRunningDuringTimeInterval:(double)arg2; +- (void)terminationTrackedForApplicationProcess:(id)arg1; +- (void)launchRequestedForApplicationProcess:(id)arg1; +- (void)_terminateApplicationProcess:(id)arg1; +- (void)terminateApplicationProcess:(id)arg1 withToken:(id)arg2; +- (id)monitoredApplicationWithProcessIdentifier:(int)arg1; +- (void)applicationWithBundleID:(id)arg1 didUpdatePID:(int)arg2 state:(unsigned long long)arg3; +- (void)_beginMonitoringApplication:(id)arg1; +- (void)setApplicationProcess:(id)arg1 forToken:(id)arg2; +- (id)applicationProcessWithToken:(id)arg1; +- (void)setApplicationProcess:(id)arg1 forPID:(int)arg2; +- (id)applicationProcessWithPID:(int)arg1; +- (id)applicationImplementationForApplicationAtPath:(id)arg1 bundleID:(id)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCApplicationMonitor_iOS.h b/PrivateHeaders/XCTest/XCApplicationMonitor_iOS.h new file mode 100644 index 0000000..cd11818 --- /dev/null +++ b/PrivateHeaders/XCTest/XCApplicationMonitor_iOS.h @@ -0,0 +1,18 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCApplicationMonitor_iOS : XCApplicationMonitor +{ +} + +- (void)_terminateApplicationProcess:(id)arg1; +- (id)monitoredApplicationWithProcessIdentifier:(int)arg1; +- (void)_beginMonitoringApplication:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCApplicationQuery.h b/PrivateHeaders/XCTest/XCApplicationQuery.h new file mode 100644 index 0000000..5b4933c --- /dev/null +++ b/PrivateHeaders/XCTest/XCApplicationQuery.h @@ -0,0 +1,23 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class XCUIApplication; + +@interface XCApplicationQuery : XCUIElementQuery +{ + XCUIApplication *_application; + id/*XCElementSnapshot*/ _lastSnapshot; +} + +@property(retain) id/*XCElementSnapshot*/ lastSnapshot; // @synthesize lastSnapshot=_lastSnapshot; +- (id)matchingSnapshotsWithError:(id *)arg1; +- (id)application; +- (id)initWithApplication:(id)arg1; + + +@end diff --git a/PrivateHeaders/XCTest/XCDebugLogDelegate-Protocol.h b/PrivateHeaders/XCTest/XCDebugLogDelegate-Protocol.h new file mode 100644 index 0000000..eb921e6 --- /dev/null +++ b/PrivateHeaders/XCTest/XCDebugLogDelegate-Protocol.h @@ -0,0 +1,11 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString; + +@protocol XCDebugLogDelegate +- (void)logDebugMessage:(NSString *)arg1; +@end diff --git a/PrivateHeaders/XCTest/XCDeviceEvent.h b/PrivateHeaders/XCTest/XCDeviceEvent.h new file mode 100644 index 0000000..db44a72 --- /dev/null +++ b/PrivateHeaders/XCTest/XCDeviceEvent.h @@ -0,0 +1,27 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface XCDeviceEvent : NSObject +{ + unsigned int _eventPage; + unsigned int _usage; + double _duration; + unsigned long long _type; + double _rotation; +} +@property unsigned long long type; // @synthesize type=_type; +@property double rotation; // @synthesize rotation=_rotation; +@property double duration; // @synthesize duration=_duration; +@property unsigned int usage; // @synthesize usage=_usage; +@property unsigned int eventPage; // @synthesize eventPage=_eventPage; +@property(readonly) BOOL isButtonHoldEvent; + ++ (id)deviceEventForDigitalCrownRotation:(double)arg1 velocity:(double)arg2; ++ (id)deviceEventWithPage:(unsigned int)arg1 usage:(unsigned int)arg2 duration:(double)arg3; + +- (void)dispatch; + +@end diff --git a/PrivateHeaders/XCTest/XCEventGenerator.h b/PrivateHeaders/XCTest/XCEventGenerator.h new file mode 100644 index 0000000..23ae00f --- /dev/null +++ b/PrivateHeaders/XCTest/XCEventGenerator.h @@ -0,0 +1,73 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import + +#import + +@class XCSynthesizedEventRecord; + +typedef void (^XCEventGeneratorHandler)(XCSynthesizedEventRecord *record, NSError *error); + +@interface XCEventGenerator : NSObject +{ + NSObject *_eventQueue; + struct __CFRunLoopObserver *_generationObserver; + unsigned long long _generation; +} + ++ (id)sharedGenerator; +@property unsigned long long generation; // @synthesize generation=_generation; +//@property(readonly) NSObject *eventQueue; // @synthesize eventQueue=_eventQueue; + +#if TARGET_OS_TV +// TODO: tvOS-specific headers + +#elif TARGET_OS_IPHONE +- (double)rotateInRect:(CGRect)arg1 withRotation:(double)arg2 velocity:(double)arg3 orientation:(UIInterfaceOrientation)arg4 handler:(XCEventGeneratorHandler)arg5; +- (double)pinchInRect:(CGRect)arg1 withScale:(double)arg2 velocity:(double)arg3 orientation:(UIInterfaceOrientation)arg4 handler:(XCEventGeneratorHandler)arg5; +- (double)pressAtPoint:(CGPoint)arg1 forDuration:(double)arg2 liftAtPoint:(CGPoint)arg3 velocity:(double)arg4 orientation:(UIInterfaceOrientation)arg5 name:(NSString *)arg6 handler:(XCEventGeneratorHandler)arg7; +- (double)pressAtPoint:(CGPoint)arg1 forDuration:(double)arg2 orientation:(UIInterfaceOrientation)arg3 handler:(XCEventGeneratorHandler)arg4; + +// iOS 9.x specific, gone in iOS 10.3 +- (double)tapWithNumberOfTaps:(unsigned long long)arg1 numberOfTouches:(unsigned long long)arg2 inRect:(CGRect)arg3 orientation:(UIInterfaceOrientation)arg4 handler:(XCEventGeneratorHandler)arg5; +- (double)twoFingerTapInRect:(CGRect)arg1 orientation:(UIInterfaceOrientation)arg2 handler:(XCEventGeneratorHandler)arg3; +- (double)doubleTapAtPoint:(CGPoint)arg1 orientation:(UIInterfaceOrientation)arg2 handler:(XCEventGeneratorHandler)arg3; +- (double)tapAtPoint:(CGPoint)arg1 orientation:(UIInterfaceOrientation)arg2 handler:(XCEventGeneratorHandler)arg3; + +// iOS 10.x specific +- (double)tapAtTouchLocations:(NSArray *)locations numberOfTaps:(NSInteger)numberOfTaps orientation:(UIInterfaceOrientation)orientation handler:(XCEventGeneratorHandler)handler; + +// iOS 10.3 specific +- (double)forcePressAtPoint:(struct CGPoint)arg1 orientation:(long long)arg2 handler:(CDUnknownBlockType)arg3; + +#elif TARGET_OS_MAC +- (double)sendKeyboardInputs:(id)arg1 layout:(id)arg2 handler:(CDUnknownBlockType)arg3; +- (double)sendKey:(id)arg1 modifierFlags:(unsigned long long)arg2 handler:(CDUnknownBlockType)arg3; +- (double)sendString:(id)arg1 handler:(CDUnknownBlockType)arg2; +- (double)setModifiers:(unsigned long long)arg1 merge:(BOOL)arg2 original:(unsigned long long *)arg3 handler:(CDUnknownBlockType)arg4; +- (double)sendKey:(unsigned short)arg1 down:(BOOL)arg2 modifiers:(unsigned long long)arg3 string:(id)arg4 handler:(CDUnknownBlockType)arg5; +- (double)hitKey:(unsigned short)arg1 handler:(CDUnknownBlockType)arg2; +- (double)scrollByX:(double)arg1 y:(double)arg2 handler:(CDUnknownBlockType)arg3; +- (double)clickAtPoint:(CGPoint)arg1 forDuration:(double)arg2 releaseAtPoint:(CGPoint)arg3 velocity:(double)arg4 handler:(CDUnknownBlockType)arg5; +- (double)clickAndDragFromPoint:(CGPoint)arg1 toPoint:(CGPoint)arg2 handler:(CDUnknownBlockType)arg3; +- (double)rightClickAtPoint:(CGPoint)arg1 handler:(CDUnknownBlockType)arg2; +- (double)doubleClickAtPoint:(CGPoint)arg1 handler:(CDUnknownBlockType)arg2; +- (double)clickAtPoint:(CGPoint)arg1 handler:(CDUnknownBlockType)arg2; +- (double)hoverAtPoint:(CGPoint)arg1 handler:(CDUnknownBlockType)arg2; +- (CGPoint)_currentMousePosition; +- (void)_clickMouseButton:(unsigned int)arg1 withCount:(unsigned long long)arg2 atPoint:(CGPoint)arg3 handleCompletion:(CDUnknownBlockType)arg4; +- (void)_moveMouseToPoint:(CGPoint)arg1 handleCompletion:(CDUnknownBlockType)arg2; +- (void)_postCGEvent:(struct __CGEvent *)arg1 handleCompletion:(CDUnknownBlockType)arg2; +#endif + +- (void)_startEventSequenceWithSteppingCallback:(CDUnknownBlockType)arg1; +- (void)_scheduleCallback:(CDUnknownBlockType)arg1 afterInterval:(double)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCKeyMappingPath.h b/PrivateHeaders/XCTest/XCKeyMappingPath.h new file mode 100644 index 0000000..541c488 --- /dev/null +++ b/PrivateHeaders/XCTest/XCKeyMappingPath.h @@ -0,0 +1,36 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSSet, NSString; + +@interface XCKeyMappingPath : NSObject +{ + unsigned long long _keyState; + XCKeyMappingPath *_next; + NSSet *_inputs; + NSString *_output; + unsigned long long _length; + NSString *_producedString; +} +@property(readonly, copy) NSString *producedString; // @synthesize producedString=_producedString; +@property(readonly) unsigned long long length; // @synthesize length=_length; +@property(readonly, copy) NSString *output; // @synthesize output=_output; +@property(readonly, copy) NSSet *inputs; // @synthesize inputs=_inputs; +@property(readonly, copy) XCKeyMappingPath *next; // @synthesize next=_next; +@property(readonly) unsigned long long keyState; // @synthesize keyState=_keyState; +@property(readonly, getter=isEmpty) BOOL empty; +@property(readonly, getter=isComplete) BOOL complete; + ++ (id)pathWithKeyState:(unsigned long long)arg1 next:(id)arg2 inputs:(id)arg3 output:(id)arg4; ++ (id)emptyPath; + +- (id)inputSequenceWithRequiredFlags:(unsigned long long)arg1 excludedFlags:(unsigned long long)arg2; +- (id)inputWithRequiredFlags:(unsigned long long)arg1 excludedFlags:(unsigned long long)arg2; + +- (id)initWithKeyState:(unsigned long long)arg1 next:(id)arg2 inputs:(id)arg3 output:(id)arg4; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCKeyboardInputSolver.h b/PrivateHeaders/XCTest/XCKeyboardInputSolver.h new file mode 100644 index 0000000..705dc31 --- /dev/null +++ b/PrivateHeaders/XCTest/XCKeyboardInputSolver.h @@ -0,0 +1,41 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSMutableArray, NSMutableDictionary, NSString, XCKeyboardKeyMap; + +@interface XCKeyboardInputSolver : NSObject +{ + XCKeyboardKeyMap *_keyMap; + NSString *_string; + unsigned long long _requiredFlags; + unsigned long long _excludedFlags; + unsigned long long _currentFlags; + BOOL _includeModifierKeys; + struct _NSRange _unsolvedRange; + NSMutableArray *_solvedInputs; + NSMutableDictionary *_solvingPaths; +} + +@property(readonly) NSArray *solvedInputs; // @synthesize solvedInputs=_solvedInputs; +@property(readonly) struct _NSRange unsolvedRange; // @synthesize unsolvedRange=_unsolvedRange; +@property BOOL includeModifierKeys; // @synthesize includeModifierKeys=_includeModifierKeys; +@property unsigned long long currentFlags; // @synthesize currentFlags=_currentFlags; +@property unsigned long long excludedFlags; // @synthesize excludedFlags=_excludedFlags; +@property unsigned long long requiredFlags; // @synthesize requiredFlags=_requiredFlags; +@property(readonly, copy) NSString *string; // @synthesize string=_string; +@property(readonly) XCKeyboardKeyMap *keyMap; // @synthesize keyMap=_keyMap; +@property(readonly, getter=isComplete) BOOL complete; + +- (id)_solve; +- (id)solve; +- (void)solveWithSolutionRange:(struct _NSRange)arg1 results:(id)arg2; +- (id)extractCompletePathsWithSolutionRange:(struct _NSRange)arg1; +- (unsigned long long)advancePaths; +- (void)advancePath:(id)arg1 range:(id)arg2; +- (id)initWithKeyMap:(id)arg1 string:(id)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCKeyboardKeyMap.h b/PrivateHeaders/XCTest/XCKeyboardKeyMap.h new file mode 100644 index 0000000..1496562 --- /dev/null +++ b/PrivateHeaders/XCTest/XCKeyboardKeyMap.h @@ -0,0 +1,101 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + + + +@class NSCharacterSet, NSData, NSDictionary, NSIndexSet, NSSet; + +@interface XCKeyboardKeyMap : NSObject +{ + struct __GSKeyboard *_inputSource; + NSData *_layoutData; + const struct { + unsigned short _field1; + unsigned short _field2; + unsigned int _field3; + unsigned int _field4; + CDStruct_a561fd19 _field5[1]; + } *_layoutHeader; + const CDStruct_a561fd19 *_keyboardType; + const struct { + unsigned short _field1; + unsigned short _field2; + unsigned int _field3; + unsigned char _field4[1]; + } *_keyModifiersToTableNum; + const struct { + unsigned short _field1; + unsigned short _field2; + unsigned int _field3; + unsigned int _field4[1]; + } *_keyToCharTableIndex; + const struct { + unsigned short _field1; + unsigned short _field2; + unsigned int _field3[1]; + } *_keyStateRecordsIndex; + const CDStruct_27a325c0 *_keyStateTerminators; + const CDStruct_27a325c0 *_keySequenceDataIndex; + NSSet *_numericPadKeyCodes; + NSDictionary *_systemKeyForKeyCode; + NSDictionary *_inputsForSystemKey; + NSDictionary *_inputForKey; + unsigned long long _longestSystemKey; + NSDictionary *_modifiersForTableID; + NSCharacterSet *_validKeyOutputIDs; + NSDictionary *_inputsForKeyOutputID; + NSSet *_safeTerminationInputs; + struct _NSRange _keyStateOutputIDsRange; + NSIndexSet *_keyStatesWithTerminator; + NSCharacterSet *_validKeyStates; + NSCharacterSet *_validSequenceIDs; + BOOL _canEmitSequenceIDAndKeyState; + NSDictionary *_inexactSequencesNFC; + unsigned long long _longestInexactSequence; + NSDictionary *_stringsForIntendedStrings; +} +@property(readonly, getter=isPrimary) BOOL primary; +@property(readonly) BOOL canEmitSequenceIDAndKeyState; // @synthesize canEmitSequenceIDAndKeyState=_canEmitSequenceIDAndKeyState; + +- (id)stringForIntendedString:(id)arg1; +- (id)stringForInputs:(id)arg1; +- (id)stringForInput:(id)arg1; +- (id)_stringForInput:(id)arg1 keyState:(unsigned long long *)arg2 output:(id)arg3; +- (void)addCachedPaths:(id)arg1 endingString:(id)arg2 range:(struct _NSRange)arg3; +- (id)cachedPathsEndingString:(id)arg1 range:(struct _NSRange)arg2; +- (void)_pathsForSequenceID:(unsigned short)arg1 range:(id)arg2 nextPath:(id)arg3 results:(id)arg4; +- (BOOL)_pathsForSystemKeyEndingString:(id)arg1 range:(struct _NSRange)arg2 nextPath:(id)arg3 results:(id)arg4; +- (id)pathsEndingString:(id)arg1 range:(id)arg2 nextPath:(id)arg3; +- (id)_pathByTerminatingKeyState:(unsigned short)arg1 next:(id)arg2 output:(id)arg3 sequenceID:(unsigned short)arg4; +- (id)pathsForSequenceID:(unsigned short)arg1 nextPath:(id)arg2; +- (void)_sequenceIDsEndingString:(id)arg1 range:(struct _NSRange)arg2 suffixRange:(struct _NSRange)arg3 results:(id)arg4; +- (id)sequenceIDsEndingString:(id)arg1 range:(struct _NSRange)arg2; +- (id)sequenceIDsForString:(id)arg1 range:(struct _NSRange)arg2; +- (id)sequenceIDsForString:(id)arg1; +- (id)stringForSequenceID:(unsigned short)arg1; +- (id)inputsForOutputID:(unsigned short)arg1; +- (id)inputsForText:(id)arg1 currentFlags:(unsigned long long)arg2; +- (id)inputsForText:(id)arg1; +- (id)inputsToSetModifierFlags:(unsigned long long)arg1 currentFlags:(unsigned long long)arg2; +- (id)inputForKey:(id)arg1 modifierFlags:(unsigned long long)arg2; +- (BOOL)canEmitKeyState:(unsigned short)arg1; +- (BOOL)canEmitSequenceIDAsOutputID:(unsigned short)arg1; +- (BOOL)canEmitSequenceID:(unsigned short)arg1; +- (BOOL)canEmitOutputID:(unsigned short)arg1; +- (unsigned long long)uniqueKeyboardType:(unsigned long long)arg1; +- (BOOL)supportsKeyboardType:(unsigned long long)arg1; + +- (void)_initIntendedStrings; +- (void)_initInexactSequences; +- (void)_initValidity; +- (void)_initKeyStates; +- (void)_initKeyOutputs; +- (void)_initModifiers; +- (void)_initKeyboardKeys; +- (id)initWithInputSource:(struct __GSKeyboard *)arg1 layoutData:(id)arg2 index:(unsigned long long)arg3; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCKeyboardLayout.h b/PrivateHeaders/XCTest/XCKeyboardLayout.h new file mode 100644 index 0000000..ff570af --- /dev/null +++ b/PrivateHeaders/XCTest/XCKeyboardLayout.h @@ -0,0 +1,35 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSData, NSString, XCKeyboardKeyMap; + +@interface XCKeyboardLayout : NSObject +{ + struct __GSKeyboard *_source; + NSString *_identifier; + NSData *_data; + NSArray *_keyMaps; + XCKeyboardKeyMap *_primaryKeyMap; +} +@property(readonly) XCKeyboardKeyMap *primaryKeyMap; // @synthesize primaryKeyMap=_primaryKeyMap; +@property(readonly, copy) NSString *identifier; // @synthesize identifier=_identifier; + ++ (id)unicodeHexKeyboardLayout; ++ (id)currentKeyboardLayout; ++ (void)enumerateKeyboardLayoutsUsingBlock:(CDUnknownBlockType)arg1; ++ (id)keyboardLayoutWithInputSource:(struct __GSKeyboard *)arg1; ++ (id)keyboardLayoutWithIdentifier:(id)arg1; + +- (BOOL)deactivate:(id)arg1 error:(id *)arg2; +- (id)activateWithError:(id *)arg1; +- (id)_setActiveLayoutState:(id)arg1 error:(id *)arg2; +- (void)enumerateKeyMapsUsingBlock:(CDUnknownBlockType)arg1; +- (id)keyMapForKeyboardType:(unsigned long long)arg1; + +- (id)initWithInputSource:(struct __GSKeyboard *)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCPointerEvent.h b/PrivateHeaders/XCTest/XCPointerEvent.h new file mode 100644 index 0000000..e40079d --- /dev/null +++ b/PrivateHeaders/XCTest/XCPointerEvent.h @@ -0,0 +1,30 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface XCPointerEvent : NSObject +{ + unsigned long long _eventType; + unsigned long long _buttonType; + double _pressure; + double _offset; + struct CGPoint _coordinate; +} +@property double offset; // @synthesize offset=_offset; +@property double pressure; // @synthesize pressure=_pressure; +@property struct CGPoint coordinate; // @synthesize coordinate=_coordinate; +@property unsigned long long buttonType; // @synthesize buttonType=_buttonType; +@property unsigned long long eventType; // @synthesize eventType=_eventType; + ++ (CDUnknownBlockType)offsetComparator; ++ (id)pointerEventWithType:(unsigned long long)arg1 buttonType:(unsigned long long)arg2 coordinate:(struct CGPoint)arg3 pressure:(double)arg4 offset:(double)arg5; ++ (id)pointerEventWithType:(unsigned long long)arg1 buttonType:(unsigned long long)arg2 coordinate:(struct CGPoint)arg3 offset:(double)arg4; +// available since Xcode 10.2 ++ (id)keyboardEventForKeyCode:(unsigned long long)arg1 keyPhase:(unsigned long long)arg2 modifierFlags:(unsigned long long)arg3 offset:(double)arg4; + + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCPointerEventPath.h b/PrivateHeaders/XCTest/XCPointerEventPath.h new file mode 100644 index 0000000..26b289b --- /dev/null +++ b/PrivateHeaders/XCTest/XCPointerEventPath.h @@ -0,0 +1,43 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSMutableArray; + +@interface XCPointerEventPath : NSObject +{ + NSMutableArray *_pointerEvents; + BOOL _immutable; + unsigned long long _pathType; + unsigned long long _index; +} +@property BOOL immutable; // @synthesize immutable=_immutable; +@property unsigned long long index; // @synthesize index=_index; +@property(readonly) unsigned long long pathType; // @synthesize pathType=_pathType; +@property(readonly) NSArray *pointerEvents; + +- (id)firstEventAfterOffset:(double)arg1; +- (id)lastEventBeforeOffset:(double)arg1; +- (void)_addPointerEvent:(id)arg1; +- (void)releaseButton:(unsigned long long)arg1 atOffset:(double)arg2; +- (void)pressButton:(unsigned long long)arg1 atOffset:(double)arg2; +- (void)liftUpAtOffset:(double)arg1; +- (void)moveToPoint:(struct CGPoint)arg1 atOffset:(double)arg2; +- (void)pressDownWithPressure:(double)arg1 atOffset:(double)arg2; +- (void)pressDownAtOffset:(double)arg1; +- (id)initForMouseAtPoint:(struct CGPoint)arg1 offset:(double)arg2; +- (id)initForTouchAtPoint:(CGPoint)arg1 offset:(double)arg2; +// Since Xcode 10.2 +- (void)typeKey:(id)arg1 modifiers:(unsigned long long)arg2 atOffset:(double)arg3; +// Since Xcode 12.beta5 +- (void)typeText:(id)arg1 atOffset:(double)arg2 typingSpeed:(unsigned long long)arg3 shouldRedact:(_Bool)arg4; +// Since Xcode 10.2 +- (id)initForTextInput; +// Since Xcode 10.2 +- (void)setModifiers:(unsigned long long)arg1 mergeWithCurrentModifierFlags:(_Bool)arg2 atOffset:(double)arg3; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCSourceCodeRecording.h b/PrivateHeaders/XCTest/XCSourceCodeRecording.h new file mode 100644 index 0000000..d727582 --- /dev/null +++ b/PrivateHeaders/XCTest/XCSourceCodeRecording.h @@ -0,0 +1,42 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSMutableArray, NSMutableDictionary, NSMutableSet; + +@interface XCSourceCodeRecording : NSObject +{ + unsigned long long _language; + NSMutableArray *_treeNodes; + NSMutableSet *_variableTreeNodes; + NSArray *_reservedNames; + NSMutableDictionary *_variableNameToContentNodeDictionary; + long long _nextVariableCount; +} + +@property(retain) NSMutableDictionary *variableNameToContentNodeDictionary; // @synthesize variableNameToContentNodeDictionary=_variableNameToContentNodeDictionary; +@property(retain, setter=_setTreeNodes:) NSArray *_treeNodes; // @synthesize _treeNodes; +@property(readonly) unsigned long long language; // @synthesize language=_language; +- (BOOL)_shareLongestCommonSection_StartAtIndex:(long long)arg1 nextCandidateIndex:(long long *)arg2; +- (BOOL)_createAndShareLocalVariableUsingSourceNode:(id)arg1 atIndex:(long long)arg2; +- (id)_variableNameForVariableContentNode:(id)arg1; +- (unsigned long long)_variableClassTypeForVariableContentNode:(id)arg1; +- (id)_variableSuffixForElementType:(unsigned long long)arg1 classType:(unsigned long long)arg2; +- (id)_transformedVariablePrefixForLabel:(id)arg1; +- (id)_variableNameForElementType:(unsigned long long)arg1 label:(id)arg2 classType:(unsigned long long)arg3; +- (id)_uniqueVariableNameWithName:(id)arg1; +- (id)_nodes:(id)arg1 matchingDistanceFromRoot:(BOOL)arg2 variableContentNode:(id)arg3 withVariableName:(id)arg4 startingIndex:(long long)arg5 replacedNodes:(long long *)arg6 indexOfFirstReplacedNode:(long long *)arg7; +- (BOOL)_shareCommonSectionsUsingExistingLocalVariables; +- (void)_shareCommonSectionsInLocalVariables; +- (id)variableNodeForNode:(id)arg1 withName:(id)arg2 variableType:(unsigned long long)arg3; +- (id)_sourceCodePrefixForVariableName:(id)arg1 variableType:(unsigned long long)arg2; +- (id)_stringRepresentationWithOptions:(unsigned long long)arg1 error:(id *)arg2; +- (id)stringRepresentationWithError:(id *)arg1; +- (void)appendNode:(id)arg1 replaceLastNode:(BOOL)arg2; +- (id)copy; +- (id)initWithLanguage:(unsigned long long)arg1 reservedNames:(id)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCSourceCodeTreeNode.h b/PrivateHeaders/XCTest/XCSourceCodeTreeNode.h new file mode 100644 index 0000000..9a850a6 --- /dev/null +++ b/PrivateHeaders/XCTest/XCSourceCodeTreeNode.h @@ -0,0 +1,84 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSIndexPath, NSNumber, NSSet, NSString; + +@interface XCSourceCodeTreeNode : NSObject +{ + NSString *_sourceCodePrefix; + NSString *_sourceCodeSuffix; + NSArray *_childNodes; + long long _selectedChildNodeIndex; + XCSourceCodeTreeNode *_parentNode; + NSSet *_identifierValues; + NSNumber *_index; + NSString *_queryType; + NSNumber *_returnType; + NSNumber *_calleeType; + NSNumber *_elementType; +} +@property(copy, setter=_setElementType:) NSNumber *_elementType; // @synthesize _elementType; +@property(copy, setter=_setCalleeType:) NSNumber *_calleeType; // @synthesize _calleeType; +@property(copy, setter=_setReturnType:) NSNumber *_returnType; // @synthesize _returnType; +@property(copy, setter=_setQueryType:) NSString *_queryType; // @synthesize _queryType; +@property(copy, setter=_setIndex:) NSNumber *_index; // @synthesize _index; +@property(copy, setter=_setIdentifierValues:) NSSet *_identifierValues; // @synthesize _identifierValues; +@property(retain) XCSourceCodeTreeNode *selectedChildNode; +@property(readonly) NSIndexPath *selectedChildNodeIndexPath; +@property unsigned long long selectedChildNodeIndex; +@property(retain) NSArray *childNodes; +@property(copy) NSString *sourceCodeSuffix; +@property(copy) NSString *sourceCodePrefix; +@property __weak XCSourceCodeTreeNode *parentNode; +@property(readonly) XCSourceCodeTreeNode *rootNode; +@property(readonly, copy) NSString *displayName; + ++ (id)_stringRepresentationsOfNodesAsSeparateLines:(id)arg1 language:(unsigned long long)arg2 options:(unsigned long long)arg3 error:(id *)arg4; ++ (id)stringRepresentationsOfNodesAsSeparateLines:(id)arg1 language:(unsigned long long)arg2 error:(id *)arg3; ++ (unsigned long long)_defaultOptions; ++ (id)treeForStringRepresentation:(id)arg1 range:(struct _NSRange)arg2 error:(id *)arg3; ++ (struct _NSRange)_rangeOfFirstSourceCodeTreeInString:(id)arg1 range:(struct _NSRange)arg2 compiledSourceCodeRange:(struct _NSRange *)arg3 jsonRange:(struct _NSRange *)arg4; ++ (struct _NSRange)rangeOfFirstSourceCodeTreeInString:(id)arg1 range:(struct _NSRange)arg2; ++ (id)_sourceCodeForNodes:(id)arg1 error:(id *)arg2; ++ (BOOL)_isContentOfNodesArraysEqual:(id)arg1 ignoringSelection:(BOOL)arg2 toDistanceFromRoot:(long long)arg3; ++ (BOOL)_isContentOfNodesEqual:(id)arg1 ignoringSelection:(BOOL)arg2 toDistanceFromRoot:(long long)arg3; ++ (BOOL)_isContentEqualIgnoringSelection:(BOOL)arg1 childNodes:(id)arg2 childNodes:(id)arg3 toDistanceFromRoot:(long long)arg4; ++ (id)_nodesByMergingSimilarNodes:(id)arg1; ++ (void)_shareSourceCodeStringsForNodes:(id)arg1; + +- (void)_absorbOnlyChildrenIntoParents; +- (id)_treeByPushingOutPrefix:(id *)arg1 error:(id *)arg2; +- (id)copy; +- (id)_copyIncludingNodesWithDistanceFromRoot:(long long)arg1 passingTest:(CDUnknownBlockType)arg2; +- (id)_copyIncludingNodesWithDistanceFromRoot:(unsigned long long)arg1 descendantChildrenArrays:(id)arg2 selectedChildNodeIndexes:(id)arg3; +- (id)_copyIncludingNodesWithMinimumDistanceFromLeaf:(unsigned long long)arg1 descendantChildrenArrays:(id)arg2 selectedChildNodeIndexes:(id)arg3; +- (BOOL)_canPushPutSolitaryRootNodes; +- (unsigned long long)_distanceFromRoot; +- (unsigned long long)_minimumDistanceFromLeaf; +- (unsigned long long)_maximumDistanceFromLeaf; +- (id)_stringRepresentationWithCompiledCodeRange:(struct _NSRange *)arg1 options:(unsigned long long)arg2 error:(id *)arg3; +- (id)_stringRepresentationWithOptions:(unsigned long long)arg1 error:(id *)arg2; +- (BOOL)_leavesHaveNoNonLeafSiblingsAndHaveSamePrefix:(id *)arg1 suffix:(id *)arg2; +- (BOOL)_leavesHaveSameAccumulatedPrefix:(id *)arg1; +- (id)stringRepresentationWithCompiledCodeRange:(struct _NSRange *)arg1 error:(id *)arg2; +- (id)stringRepresentationWithError:(id *)arg1; +- (id)initWithCoder:(id)arg1; +- (void)encodeWithCoder:(id)arg1; +- (id)_treeAsJSONWithError:(id *)arg1; +- (id)descriptionWithDepth:(unsigned long long)arg1; +- (id)_depthStringWithDepth:(unsigned long long)arg1; +- (id)sourceCodeForAllDescendants; +- (id)selectedDescendantsSourceCodeWithError:(id *)arg1; +- (id)selectedChildNodesIndexesWithError:(id *)arg1; +- (void)setChildrenOnAllLeafNodes:(id)arg1 selectChildNodeIndex:(unsigned long long)arg2; +- (BOOL)_isContentEqual:(id)arg1 ignoringSelection:(BOOL)arg2 toDistanceFromRoot:(unsigned long long)arg3; +- (unsigned long long)_descendantCount; +- (BOOL)setChildNodes:(id)arg1 error:(id *)arg2; +- (BOOL)_canHaveSiblingNode:(id)arg1; +- (id)initWithSourceCodePrefix:(id)arg1 sourceCodeSuffix:(id)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCSourceCodeTreeNodeEnumerator.h b/PrivateHeaders/XCTest/XCSourceCodeTreeNodeEnumerator.h new file mode 100644 index 0000000..7f82f72 --- /dev/null +++ b/PrivateHeaders/XCTest/XCSourceCodeTreeNodeEnumerator.h @@ -0,0 +1,18 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableArray; + +@interface XCSourceCodeTreeNodeEnumerator : NSObject +{ + NSMutableArray *_remainingNodes; +} + +- (id)nextObject; +- (id)initWithNode:(id)arg1; + +@end + diff --git a/PrivateHeaders/XCTest/XCSymbolicationRecord.h b/PrivateHeaders/XCTest/XCSymbolicationRecord.h new file mode 100644 index 0000000..b9be46d --- /dev/null +++ b/PrivateHeaders/XCTest/XCSymbolicationRecord.h @@ -0,0 +1,29 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString; + +@interface XCSymbolicationRecord : NSObject +{ + unsigned long long _lineNumber; + NSString *_filePath; + NSString *_symbolName; + NSString *_symbolOwner; +} +@property(copy) NSString *symbolOwner; // @synthesize symbolOwner=_symbolOwner; +@property(copy) NSString *symbolName; // @synthesize symbolName=_symbolName; +@property(copy) NSString *filePath; // @synthesize filePath=_filePath; +@property unsigned long long lineNumber; // @synthesize lineNumber=_lineNumber; + ++ (id)symbolicationRecordFromRemoteServiceForAddress:(unsigned long long)arg1; ++ (id)symbolicationRecordForTask:(unsigned int)arg1 address:(unsigned long long)arg2; ++ (id)symbolicationRecordForAddress:(unsigned long long)arg1; ++ (void)_setCurrentProcessIsRemoteService; ++ (id)_symbolicationRecordForSymbolicator:(struct _CSTypeRef)arg1 address:(unsigned long long)arg2; ++ (id)failureRecord; ++ (BOOL)softLinkCoreSymbolication; + +@end diff --git a/PrivateHeaders/XCTest/XCSymbolicatorHolder.h b/PrivateHeaders/XCTest/XCSymbolicatorHolder.h new file mode 100644 index 0000000..617c76c --- /dev/null +++ b/PrivateHeaders/XCTest/XCSymbolicatorHolder.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface XCSymbolicatorHolder : NSObject +{ + struct _CSTypeRef _symbolicator; +} + +@property struct _CSTypeRef symbolicator; // @synthesize symbolicator=_symbolicator; + +@end diff --git a/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h b/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h new file mode 100644 index 0000000..d630eeb --- /dev/null +++ b/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h @@ -0,0 +1,32 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSMutableArray, NSString, XCPointerEventPath; + +@interface XCSynthesizedEventRecord : NSObject +{ + NSMutableArray *_eventPaths; + NSString *_name; +#if !TARGET_OS_TV + UIInterfaceOrientation _interfaceOrientation; +#endif +} +#if !TARGET_OS_TV +@property(readonly) UIInterfaceOrientation interfaceOrientation; // @synthesize interfaceOrientation=_interfaceOrientation; +#endif +@property(readonly, copy) NSString *name; // @synthesize name=_name; +@property(readonly) double maximumOffset; +@property(readonly) NSArray *eventPaths; + +- (void)addPointerEventPath:(XCPointerEventPath *)arg1; +#if !TARGET_OS_TV +- (id)initWithName:(NSString *)arg1 interfaceOrientation:(UIInterfaceOrientation)arg2; +#endif +- (id)initWithName:(id)arg1; +- (id)init; +- (BOOL)synthesizeWithError:(NSError **)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTAXClient-Protocol.h b/PrivateHeaders/XCTest/XCTAXClient-Protocol.h new file mode 100644 index 0000000..45e0d35 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTAXClient-Protocol.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSData; + +@protocol XCTAXClient +- (void)handleAccessibilityNotification:(int)arg1 withPayload:(NSData *)arg2; +@end + diff --git a/PrivateHeaders/XCTest/XCTAsyncActivity-Protocol.h b/PrivateHeaders/XCTest/XCTAsyncActivity-Protocol.h new file mode 100644 index 0000000..1ec110e --- /dev/null +++ b/PrivateHeaders/XCTest/XCTAsyncActivity-Protocol.h @@ -0,0 +1,16 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSError; + +@protocol XCTAsyncActivity +@property(readonly) BOOL timedOut; +@property(readonly) NSError *error; +- (void)finishWithError:(NSError *)arg1; +@end + diff --git a/PrivateHeaders/XCTest/XCTAsyncActivity.h b/PrivateHeaders/XCTest/XCTAsyncActivity.h new file mode 100644 index 0000000..fcc72cb --- /dev/null +++ b/PrivateHeaders/XCTest/XCTAsyncActivity.h @@ -0,0 +1,23 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import "XCTAsyncActivity.h" + +@class NSError, NSString; + +@interface XCTAsyncActivity : XCTestExpectation +{ + NSError *_error; + BOOL _timedOut; +} +@property BOOL timedOut; // @synthesize timedOut=_timedOut; +@property(retain) NSError *error; // @synthesize error=_error; + +- (void)finishWithError:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTAutomationTarget-Protocol.h b/PrivateHeaders/XCTest/XCTAutomationTarget-Protocol.h new file mode 100644 index 0000000..7855915 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTAutomationTarget-Protocol.h @@ -0,0 +1,12 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@protocol XCTAutomationTarget +- (void)requestHostAppExecutableNameWithReply:(void (^)(NSString *))arg1; +@end + diff --git a/PrivateHeaders/XCTest/XCTDarwinNotificationExpectation.h b/PrivateHeaders/XCTest/XCTDarwinNotificationExpectation.h new file mode 100644 index 0000000..9508703 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTDarwinNotificationExpectation.h @@ -0,0 +1,23 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSString, _XCTDarwinNotificationExpectationImplementation; + +@interface XCTDarwinNotificationExpectation : XCTestExpectation +{ + id _internal; +} +@property(retain) _XCTDarwinNotificationExpectationImplementation *internal; // @synthesize internal=_internal; +@property(copy) CDUnknownBlockType handler; +@property(readonly, copy) NSString *notificationName; + +- (void)cleanup; +- (void)fulfill; +- (id)initWithNotificationName:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTElementSetTransformer-Protocol.h b/PrivateHeaders/XCTest/XCTElementSetTransformer-Protocol.h new file mode 100644 index 0000000..0585fb8 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTElementSetTransformer-Protocol.h @@ -0,0 +1,18 @@ +// +// Generated by class-dump 3.5 (64 bit) (Debug version compiled Jun 9 2015 22:53:21). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2014 by Steve Nygard. +// + +@class NSOrderedSet, NSSet, NSString; +@protocol XCTMatchingElementIterator; + +@protocol XCTElementSetTransformer +@property BOOL stopsOnFirstMatch; +@property(readonly) BOOL supportsAttributeKeyPathAnalysis; +@property(copy) NSString *transformationDescription; +@property(readonly) BOOL supportsRemoteEvaluation; +- (NSSet *)requiredKeyPathsOrError:(id *)arg1; +- (id )iteratorForInput:(id/*XCElementSnapshot*/)arg1; +- (NSOrderedSet *)transform:(NSOrderedSet *)arg1 relatedElements:(id *)arg2; +@end diff --git a/PrivateHeaders/XCTest/XCTKVOExpectation.h b/PrivateHeaders/XCTest/XCTKVOExpectation.h new file mode 100644 index 0000000..6546f4f --- /dev/null +++ b/PrivateHeaders/XCTest/XCTKVOExpectation.h @@ -0,0 +1,28 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSString, _XCKVOExpectationImplementation; + +@interface XCTKVOExpectation : XCTestExpectation +{ + id _internal; +} +@property(retain) _XCKVOExpectationImplementation *internal; // @synthesize internal=_internal; +@property(copy) CDUnknownBlockType handler; +@property(readonly) unsigned long long options; +@property(readonly) id expectedValue; +@property(readonly) id observedObject; +@property(readonly, copy) NSString *keyPath; + +- (void)cleanup; +- (void)fulfill; +- (id)initWithKeyPath:(id)arg1 object:(id)arg2; +- (id)initWithKeyPath:(id)arg1 object:(id)arg2 expectedValue:(id)arg3; +- (id)initWithKeyPath:(id)arg1 object:(id)arg2 expectedValue:(id)arg3 options:(unsigned long long)arg4; + +@end diff --git a/PrivateHeaders/XCTest/XCTMetric.h b/PrivateHeaders/XCTest/XCTMetric.h new file mode 100644 index 0000000..cb24c07 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTMetric.h @@ -0,0 +1,29 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSArray, NSDictionary, NSString; + +@interface XCTMetric : NSObject +{ + NSString *_identifier; + NSString *_name; + NSString *_units; + NSDictionary *_baseline; + NSDictionary *_defaultBaseline; + NSArray *_measurements; +} +@property(copy) NSArray *measurements; // @synthesize measurements=_measurements; +@property(copy) NSDictionary *defaultBaseline; // @synthesize defaultBaseline=_defaultBaseline; +@property(copy) NSDictionary *baseline; // @synthesize baseline=_baseline; +@property(copy) NSString *units; // @synthesize units=_units; +@property(copy) NSString *name; // @synthesize name=_name; +@property(copy) NSString *identifier; // @synthesize identifier=_identifier; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTNSNotificationExpectation.h b/PrivateHeaders/XCTest/XCTNSNotificationExpectation.h new file mode 100644 index 0000000..0ea1859 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTNSNotificationExpectation.h @@ -0,0 +1,27 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSNotificationCenter, NSString, _XCTNSNotificationExpectationImplementation; + +@interface XCTNSNotificationExpectation : XCTestExpectation +{ + id _internal; +} +@property(retain) _XCTNSNotificationExpectationImplementation *internal; // @synthesize internal=_internal; +@property(copy) CDUnknownBlockType handler; +@property(readonly) NSNotificationCenter *notificationCenter; +@property(readonly, copy) NSString *notificationName; +@property(readonly) id observedObject; + +- (void)cleanup; +- (void)fulfill; +- (id)initWithName:(id)arg1; +- (id)initWithName:(id)arg1 object:(id)arg2; +- (id)initWithName:(id)arg1 object:(id)arg2 notificationCenter:(id)arg3; + +@end diff --git a/PrivateHeaders/XCTest/XCTNSPredicateExpectation.h b/PrivateHeaders/XCTest/XCTNSPredicateExpectation.h new file mode 100644 index 0000000..cd92b57 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTNSPredicateExpectation.h @@ -0,0 +1,24 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSPredicate, _XCTNSPredicateExpectationImplementation; + +@interface XCTNSPredicateExpectation : XCTestExpectation +{ + id _internal; +} +@property(retain) _XCTNSPredicateExpectationImplementation *internal; // @synthesize internal=_internal; +@property(copy) CDUnknownBlockType handler; +@property(readonly, copy) NSPredicate *predicate; +@property(readonly) id object; + +- (void)cleanup; +- (void)fulfill; +- (id)initWithPredicate:(id)arg1 object:(id)arg2; + +@end diff --git a/PrivateHeaders/XCTest/XCTNSPredicateExpectationObject-Protocol.h b/PrivateHeaders/XCTest/XCTNSPredicateExpectationObject-Protocol.h new file mode 100644 index 0000000..9b5d195 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTNSPredicateExpectationObject-Protocol.h @@ -0,0 +1,15 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class XCTNSPredicateExpectation; + +@protocol XCTNSPredicateExpectationObject + +@optional +- (BOOL)evaluatePredicateForExpectation:(XCTNSPredicateExpectation *)arg1 debugMessage:(id *)arg2; +@end diff --git a/PrivateHeaders/XCTest/XCTRunnerAutomationSession.h b/PrivateHeaders/XCTest/XCTRunnerAutomationSession.h new file mode 100644 index 0000000..df43e79 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTRunnerAutomationSession.h @@ -0,0 +1,21 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +#import "XCTRunnerAutomationSession.h" + +@class NSString, NSXPCConnection; + +@interface XCTRunnerAutomationSession : NSObject +{ + NSXPCConnection *_connection; +} +@property NSXPCConnection *connection; // @synthesize connection=_connection; + +- (id)initWithEndpoint:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h new file mode 100644 index 0000000..7ca5303 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h @@ -0,0 +1,98 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "XCTestManager_TestsInterface-Protocol.h" +#import "XCEventGenerator.h" +#import +#import + +@class NSMutableDictionary, NSXPCConnection, XCSynthesizedEventRecord; +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +@class CLLocation; +#endif +@protocol XCTUIApplicationMonitor, XCTAXClient, XCTestManager_ManagerInterface; + +// iOS since 10.3 +@interface XCTRunnerDaemonSession : NSObject +{ + NSObject *_queue; + id _applicationMonitor; + id _accessibilityClient; + NSXPCConnection *_connection; + unsigned long long _daemonProtocolVersion; + NSMutableDictionary *_invalidationHandlers; +} +@property(retain) NSObject *queue; // @synthesize queue=_queue; +@property id accessibilityClient; // @synthesize accessibilityClient=_accessibilityClient; +@property id applicationMonitor; // @synthesize applicationMonitor=_applicationMonitor; +@property(retain) NSMutableDictionary *invalidationHandlers; // @synthesize invalidationHandlers=_invalidationHandlers; +@property(retain) NSXPCConnection *connection; // @synthesize connection=_connection; +@property(readonly) BOOL useLegacyEventCoordinateTransformationPath; +@property unsigned long long daemonProtocolVersion; +@property(readonly) id daemonProxy; + ++ (instancetype)sharedSession; + +- (void)injectVoiceRecognitionAudioInputPaths:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)injectAssistantRecognitionStrings:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)startSiriUIRequestWithAudioFileURL:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)startSiriUIRequestWithText:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)requestDTServiceHubConnectionWithReply:(CDUnknownBlockType)arg1; +- (void)enableFauxCollectionViewCells:(CDUnknownBlockType)arg1; +- (void)loadAccessibilityWithTimeout:(double)arg1 reply:(CDUnknownBlockType)arg2; +- (void)setAXTimeout:(double)arg1 reply:(CDUnknownBlockType)arg2; +- (void)requestScreenshotWithReply:(CDUnknownBlockType)arg1; +- (void)sendString:(id)arg1 maximumFrequency:(unsigned long long)arg2 completion:(CDUnknownBlockType)arg3; +- (void)updateDeviceOrientation:(long long)arg1 completion:(CDUnknownBlockType)arg2; +- (void)performDeviceEvent:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)synthesizeEvent:(XCSynthesizedEventRecord *)arg1 completion:(void (^)(NSError *))arg2; +- (void)requestElementAtPoint:(CGPoint)arg1 reply:(CDUnknownBlockType)arg2; +- (void)fetchParameterizedAttributeForElement:(id)arg1 attribute:(id)arg2 parameter:(id)arg3 reply:(CDUnknownBlockType)arg4; +- (void)setAttribute:(id)arg1 value:(id)arg2 element:(id)arg3 reply:(CDUnknownBlockType)arg4; +- (void)fetchAttributesForElement:(id)arg1 attributes:(id)arg2 reply:(CDUnknownBlockType)arg3; +- (void)snapshotForElement:(id)arg1 attributes:(id)arg2 parameters:(id)arg3 reply:(void (^)(id/*XCElementSnapshot*/, NSError *))arg4; +- (void)terminateApplicationWithBundleID:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)performAccessibilityAction:(int)arg1 onElement:(id)arg2 value:(id)arg3 reply:(CDUnknownBlockType)arg4; +- (void)unregisterForAccessibilityNotification:(int)arg1 registrationToken:(id)arg2 reply:(CDUnknownBlockType)arg3; +- (void)registerForAccessibilityNotification:(int)arg1 reply:(CDUnknownBlockType)arg2; +- (void)launchApplicationWithBundleID:(id)arg1 arguments:(id)arg2 environment:(id)arg3 completion:(CDUnknownBlockType)arg4; +- (void)startMonitoringApplicationWithBundleID:(id)arg1; +- (void)requestBackgroundAssertionForPID:(int)arg1 reply:(CDUnknownBlockType)arg2; +- (void)requestAutomationSessionForTestTargetWithPID:(int)arg1 reply:(CDUnknownBlockType)arg2; +- (void)requestIDEConnectionSocketForSessionIdentifier:(id)arg1 reply:(CDUnknownBlockType)arg2; +- (void)_XCT_receivedAccessibilityNotification:(int)arg1 withPayload:(id)arg2; +- (void)_XCT_applicationWithBundleID:(id)arg1 didUpdatePID:(int)arg2 andState:(unsigned long long)arg3; +- (void)unregisterInvalidationHandlerWithToken:(id)arg1; +- (id)registerInvalidationHandler:(CDUnknownBlockType)arg1; +- (void)_reportInvalidation; +- (id)initWithConnection:(id)arg1; + +// Since Xcode 14.3 +- (void)openURL:(NSURL *)arg1 usingApplication:(NSString *)arg2 completion:(void (^)(_Bool, NSError *))arg3; +- (void)openDefaultApplicationForURL:(NSURL *)arg1 completion:(void (^)(_Bool, NSError *))arg2; +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +- (void)setSimulatedLocation:(CLLocation *)arg1 completion:(void (^)(_Bool, NSError *))arg2; +- (void)getSimulatedLocationWithReply:(void (^)(CLLocation *, NSError *))arg1; +- (void)clearSimulatedLocationWithReply:(void (^)(_Bool, NSError *))arg1; +@property(readonly) _Bool supportsLocationSimulation; +#endif + +// Since Xcode 15.0-beta1 +- (void)stopScreenRecordingWithUUID:(NSUUID *)arg1 + withReply:(void (^)(NSError *))arg2; +- (void)startScreenRecordingWithRequest:(id/* XCTScreenRecordingRequest */)arg1 + withReply:(void (^)(id/* XCTAttachmentFutureMetadata */, NSError *))arg2; +- (_Bool)supportsScreenRecording; +- (_Bool)preferScreenshotsOverScreenRecordings; + +// Since Xcode 10.2 +- (void)launchApplicationWithPath:(NSString *)arg1 + bundleID:(NSString *)arg2 + arguments:(NSArray *)arg3 + environment:(NSDictionary *)arg4 + completion:(void (^)(_Bool, NSError *))arg5; + +@end diff --git a/PrivateHeaders/XCTest/XCTRunnerIDESession.h b/PrivateHeaders/XCTest/XCTRunnerIDESession.h new file mode 100644 index 0000000..0859afc --- /dev/null +++ b/PrivateHeaders/XCTest/XCTRunnerIDESession.h @@ -0,0 +1,65 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +#import "XCTTestRunSessionDelegate.h" +#import "XCTestDriverInterface.h" +#import "XCTestObservation.h" + +@class DTXConnection, NSObject, NSString, XCTestRun; + +@interface XCTRunnerIDESession : NSObject +{ + NSObject *_queue; + DTXConnection *_IDEConnection; + id _IDEProxy; + long long _IDEProtocolVersion; + id _applicationMonitor; + XCTestRun *_currentTestRun; + CDUnknownBlockType _readinessReply; +} +@property(copy) CDUnknownBlockType readinessReply; // @synthesize readinessReply=_readinessReply; +@property(retain) id IDEProxy; // @synthesize IDEProxy=_IDEProxy; +@property(retain) DTXConnection *IDEConnection; // @synthesize IDEConnection=_IDEConnection; +@property __weak id applicationMonitor; // @synthesize applicationMonitor=_applicationMonitor; +@property(retain) NSObject *queue; // @synthesize queue=_queue; +@property(readonly) BOOL reportsCrashes; +@property long long IDEProtocolVersion; // @synthesize IDEProtocolVersion=_IDEProtocolVersion; + ++ (int)connectedSocketForLocalPath:(id)arg1 error:(id *)arg2; ++ (void)setSharedSession:(id)arg1; ++ (id)sharedSession; ++ (id)sharedSessionQueue; + +- (void)testBundleDidFinish:(id)arg1; +- (void)_testCase:(id)arg1 didFinishActivity:(id)arg2; +- (void)_testCase:(id)arg1 willStartActivity:(id)arg2; +- (void)_testCase:(id)arg1 didMeasureValues:(id)arg2 forPerformanceMetricID:(id)arg3 name:(id)arg4 unitsOfMeasurement:(id)arg5 baselineName:(id)arg6 baselineAverage:(id)arg7 maxPercentRegression:(id)arg8 maxPercentRelativeStandardDeviation:(id)arg9 maxRegression:(id)arg10 maxStandardDeviation:(id)arg11 file:(id)arg12 line:(unsigned long long)arg13; +- (void)testCase:(id)arg1 didFailWithDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testCaseDidFinish:(id)arg1; +- (void)testCaseWillStart:(id)arg1; +- (void)testSuiteDidFinish:(id)arg1; +- (void)testSuite:(id)arg1 didFailWithDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testSuiteWillStart:(id)arg1; +- (void)testBundleWillStart:(id)arg1; +- (id)_IDE_processWithToken:(id)arg1 exitedWithStatus:(id)arg2; +- (id)_IDE_stopTrackingProcessWithToken:(id)arg1; +- (void)terminateProcessWithToken:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)requestLaunchProgressForProcessWithToken:(id)arg1 completion:(CDUnknownBlockType)arg2; +- (void)launchProcessWithPath:(id)arg1 bundleID:(id)arg2 arguments:(id)arg3 environmentVariables:(id)arg4 completion:(CDUnknownBlockType)arg5; +- (id)_IDE_processWithBundleID:(id)arg1 path:(id)arg2 pid:(id)arg3 crashedUnderSymbol:(id)arg4; +- (void)reportStallOnMainThreadInTestCase:(id)arg1 method:(id)arg2 file:(id)arg3 line:(unsigned long long)arg4; +- (void)logDebugMessage:(id)arg1; +- (void)testRunSessionDidFinishExecutingTestPlan:(id)arg1 reply:(CDUnknownBlockType)arg2; +- (void)testRunSession:(id)arg1 initializationForUITestingDidFailWithError:(id)arg2; +- (void)testRunSessionDidBeginInitializingForUITesting:(id)arg1; +- (void)testRunSessionDidBeginExecutingTestPlan:(id)arg1; +- (id)_IDE_startExecutingTestPlanWithProtocolVersion:(id)arg1; +- (void)requestReadinessForTesting:(CDUnknownBlockType)arg1; +- (id)initWithConnectedSocket:(int)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTTestRunSession.h b/PrivateHeaders/XCTest/XCTTestRunSession.h new file mode 100644 index 0000000..113e6cd --- /dev/null +++ b/PrivateHeaders/XCTest/XCTTestRunSession.h @@ -0,0 +1,25 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class XCTestConfiguration; + +@interface XCTTestRunSession : NSObject +{ + XCTestConfiguration *_testConfiguration; + id _delegate; +} +@property id delegate; // @synthesize delegate=_delegate; +@property(retain) XCTestConfiguration *testConfiguration; // @synthesize testConfiguration=_testConfiguration; + +- (BOOL)runTestsAndReturnError:(id *)arg1; +- (BOOL)_preTestingInitialization; +- (void)resumeAppSleep:(id)arg1; +- (id)suspendAppSleep; +- (id)initWithTestConfiguration:(id)arg1 delegate:(id)arg2; + +@end diff --git a/PrivateHeaders/XCTest/XCTTestRunSessionDelegate-Protocol.h b/PrivateHeaders/XCTest/XCTTestRunSessionDelegate-Protocol.h new file mode 100644 index 0000000..9dd15d3 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTTestRunSessionDelegate-Protocol.h @@ -0,0 +1,17 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSError, XCTTestRunSession; + +@protocol XCTTestRunSessionDelegate +- (void)testRunSessionDidFinishExecutingTestPlan:(XCTTestRunSession *)arg1 reply:(void (^)(void))arg2; +- (void)testRunSession:(XCTTestRunSession *)arg1 initializationForUITestingDidFailWithError:(NSError *)arg2; +- (void)testRunSessionDidBeginInitializingForUITesting:(XCTTestRunSession *)arg1; +- (void)testRunSessionDidBeginExecutingTestPlan:(XCTTestRunSession *)arg1; +@end + diff --git a/PrivateHeaders/XCTest/XCTUIApplicationMonitor-Protocol.h b/PrivateHeaders/XCTest/XCTUIApplicationMonitor-Protocol.h new file mode 100644 index 0000000..2345e4d --- /dev/null +++ b/PrivateHeaders/XCTest/XCTUIApplicationMonitor-Protocol.h @@ -0,0 +1,17 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSNumber, NSString; + +@protocol XCTUIApplicationMonitor +- (void)applicationWithBundleID:(NSString *)arg1 didUpdatePID:(int)arg2 state:(unsigned long long)arg3; +- (void)processWithToken:(NSNumber *)arg1 exitedWithStatus:(int)arg2; +- (void)stopTrackingProcessWithToken:(NSNumber *)arg1; +- (void)crashInProcessWithBundleID:(NSString *)arg1 path:(NSString *)arg2 pid:(int)arg3 symbol:(NSString *)arg4; +@end + diff --git a/PrivateHeaders/XCTest/XCTWaiter.h b/PrivateHeaders/XCTest/XCTWaiter.h new file mode 100644 index 0000000..c1487ab --- /dev/null +++ b/PrivateHeaders/XCTest/XCTWaiter.h @@ -0,0 +1,55 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +#import "XCTWaiterManagement.h" +#import "XCTestExpectationDelegate.h" + +@class NSArray, NSObject, NSString, _XCTWaiterImpl; + +@interface XCTWaiter : NSObject +{ + id _internalImplementation; +} +@property(readonly) _XCTWaiterImpl *internalImplementation; // @synthesize internalImplementation=_internalImplementation; +@property(readonly) double timeout; +@property(readonly, getter=isInProgress) BOOL inProgress; +@property struct __CFRunLoop *waitingRunLoop; +@property(readonly, nonatomic) NSObject *delegateQueue; +@property(readonly, nonatomic) NSObject *queue; +@property(readonly, copy) NSArray *waitCallStackReturnAddresses; +@property(readonly) NSArray *fulfilledExpectations; +@property __weak id delegate; + ++ (id)waitForActivity:(id)arg1 timeout:(double)arg2 block:(CDUnknownBlockType)arg3; ++ (long long)waitForExpectations:(id)arg1 timeout:(double)arg2 enforceOrder:(BOOL)arg3; ++ (long long)waitForExpectations:(id)arg1 timeout:(double)arg2; ++ (void)wait:(double)arg1; ++ (void)setStallHandler:(CDUnknownBlockType)arg1; ++ (void)handleStalledWaiter:(id)arg1; ++ (CDUnknownBlockType)installWatchdogForWaiter:(id)arg1 timeout:(double)arg2; + +- (long long)result; +- (void)setState:(long long)arg1; +- (long long)state; +- (void)setWaitCallStackReturnAddresses:(id)arg1; +- (void)_queue_validateExpectationFulfillmentWithTimeoutState:(BOOL)arg1; +- (BOOL)_queue_enforceOrderingWithFulfilledExpectations:(id)arg1; +- (void)_queue_computeInitiallyFulfilledExpectations; +- (void)_queue_setExpectations:(id)arg1; +- (void)_validateExpectationFulfillmentWithTimeoutState:(BOOL)arg1; +- (void)didFulfillExpectation:(id)arg1; +- (void)cancelPrimitiveWait; +- (void)cancelWaiting; +- (void)primitiveWait:(double)arg1; +- (void)interruptForWaiter:(id)arg1; +- (long long)waitForExpectations:(id)arg1 timeout:(double)arg2 enforceOrder:(BOOL)arg3; +- (long long)waitForExpectations:(id)arg1 timeout:(double)arg2; +- (id)initWithDelegate:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTWaiterDelegate-Protocol.h b/PrivateHeaders/XCTest/XCTWaiterDelegate-Protocol.h new file mode 100644 index 0000000..c1e8a68 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTWaiterDelegate-Protocol.h @@ -0,0 +1,16 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSArray, XCTWaiter, XCTestExpectation; + +@protocol XCTWaiterDelegate +- (void)waiter:(XCTWaiter *)arg1 didFulfillInvertedExpectation:(XCTestExpectation *)arg2; +- (void)waiter:(XCTWaiter *)arg1 fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)arg2 requiredExpectation:(XCTestExpectation *)arg3; +- (void)waiter:(XCTWaiter *)arg1 didTimeoutWithUnfulfilledExpectations:(NSArray *)arg2; +@end + diff --git a/PrivateHeaders/XCTest/XCTWaiterDelegatePrivate-Protocol.h b/PrivateHeaders/XCTest/XCTWaiterDelegatePrivate-Protocol.h new file mode 100644 index 0000000..592c468 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTWaiterDelegatePrivate-Protocol.h @@ -0,0 +1,12 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class XCTWaiter; + +@protocol XCTWaiterDelegatePrivate +- (void)nestedWaiter:(XCTWaiter *)arg1 wasInterruptedByTimedOutWaiter:(XCTWaiter *)arg2; +@end + diff --git a/PrivateHeaders/XCTest/XCTWaiterManagement-Protocol.h b/PrivateHeaders/XCTest/XCTWaiterManagement-Protocol.h new file mode 100644 index 0000000..bd78246 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTWaiterManagement-Protocol.h @@ -0,0 +1,13 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@protocol XCTWaiterManagement +@property(readonly, getter=isInProgress) BOOL inProgress; +- (void)interruptForWaiter:(id )arg1; +@end + diff --git a/PrivateHeaders/XCTest/XCTWaiterManager.h b/PrivateHeaders/XCTest/XCTWaiterManager.h new file mode 100644 index 0000000..a1d0363 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTWaiterManager.h @@ -0,0 +1,28 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSMutableArray, NSObject, NSThread; + +@interface XCTWaiterManager : NSObject +{ + NSMutableArray *_waiterStack; + NSThread *_thread; + NSObject *_queue; +} +@property(readonly) NSObject *queue; // @synthesize queue=_queue; +@property NSThread *thread; // @synthesize thread=_thread; +@property(retain) NSMutableArray *waiterStack; // @synthesize waiterStack=_waiterStack; + ++ (id)threadLocalManager; + +- (void)waiterDidFinishWaiting:(id)arg1; +- (void)waiterTimedOutWhileWaiting:(id)arg1; +- (void)waiterWillBeginWaiting:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTest.h b/PrivateHeaders/XCTest/XCTest.h new file mode 100644 index 0000000..8353666 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTest.h @@ -0,0 +1,34 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString, XCTestRun; + +@interface XCTest : NSObject +{ + id _internal; +} +@property(readonly) NSString *nameForLegacyLogging; +@property(readonly) NSString *languageAgnosticTestMethodName; +@property(readonly) NSString *languageAgnosticTestClassName; +@property(readonly) XCTestRun *testRun; +@property(readonly) Class testRunClass; +@property(readonly) Class _requiredTestRunBaseClass; +@property(readonly, copy) NSString *name; +@property(readonly) unsigned long long testCaseCount; +@property(readonly) NSString *_methodNameForReporting; +@property(readonly) NSString *_classNameForReporting; + ++ (id)languageAgnosticTestClassNameForTestClass:(Class)arg1; + +- (void)tearDown; +- (void)setUp; +- (void)runTest; +- (id)run; +- (void)performTest:(id)arg1; +- (id)init; +- (void)removeTestsWithNames:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestCase.h b/PrivateHeaders/XCTest/XCTestCase.h new file mode 100644 index 0000000..d68d949 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestCase.h @@ -0,0 +1,102 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import + +@class NSInvocation, XCTestCaseRun, XCTestContext, _XCTestCaseImplementation; + +@interface XCTestCase() +{ + id _internalImplementation; +} +@property(retain) _XCTestCaseImplementation *internalImplementation; // @synthesize internalImplementation=_internalImplementation; +@property(readonly) XCTestContext *testContext; +@property(readonly) unsigned long long activityRecordStackDepth; +@property(nonatomic) BOOL shouldHaltWhenReceivesControl; +@property(nonatomic) BOOL shouldSetShouldHaltWhenReceivesControl; // @synthesize shouldSetShouldHaltWhenReceivesControl=_shouldSetShouldHaltWhenReceivesControl; +@property(retain) XCTestCaseRun *testCaseRun; + ++ (id)_baselineDictionary; ++ (BOOL)_treatMissingBaselinesAsTestFailures; ++ (id)knownMemoryMetrics; ++ (id)measurementFormatter; ++ (BOOL)_reportPerformanceFailuresForLargeImprovements; ++ (BOOL)_enableSymbolication; + ++ (BOOL)isInheritingTestCases; ++ (id)_testStartActvityDateFormatter; ++ (id)testCaseWithSelector:(SEL)arg1; + + ++ (void)tearDown; ++ (void)setUp; ++ (id)defaultTestSuite; ++ (id)allTestMethodInvocations; ++ (void)_allTestMethodInvocations:(id)arg1; ++ (id)testMethodInvocations; ++ (id)allSubclasses; +- (void)startActivityWithTitle:(id)arg1 block:(CDUnknownBlockType)arg2; +- (void)registerDefaultMetrics; +- (id)baselinesDictionaryForTest; +- (void)_logAndReportPerformanceMetrics:(id)arg1 perfMetricResultsForIDs:(id)arg2 withBaselinesForTest:(id)arg3; +- (void)_logAndReportPerformanceMetrics:(id)arg1 perfMetricResultsForIDs:(id)arg2 withBaselinesForTest:(id)arg3 defaultBaselinesForPerfMetricID:(id)arg4; +- (void)registerMetricID:(id)arg1 name:(id)arg2 unitString:(id)arg3; +- (void)registerMetricID:(id)arg1 name:(id)arg2 unit:(id)arg3; +- (void)reportMetric:(id)arg1 reportFailures:(BOOL)arg2; +- (void)reportMeasurements:(id)arg1 forMetricID:(id)arg2 reportFailures:(BOOL)arg3; +- (void)_recordValues:(id)arg1 forPerformanceMetricID:(id)arg2 name:(id)arg3 unitsOfMeasurement:(id)arg4 baselineName:(id)arg5 baselineAverage:(id)arg6 maxPercentRegression:(id)arg7 maxPercentRelativeStandardDeviation:(id)arg8 maxRegression:(id)arg9 maxStandardDeviation:(id)arg10 file:(id)arg11 line:(unsigned long long)arg12; +- (id)_symbolicationRecordForTestCodeInAddressStack:(id)arg1; +- (void)stopMeasuring; +- (void)startMeasuring; +- (BOOL)_isMeasuringMetrics; +- (BOOL)_didStopMeasuring; +- (BOOL)_didStartMeasuring; +- (BOOL)_didMeasureMetrics; +- (id)_perfMetricsForID; +- (void)_logMemoryGraphDataFromFilePath:(id)arg1 withTitle:(id)arg2; +- (void)_logMemoryGraphData:(id)arg1 withTitle:(id)arg2; +- (unsigned long long)numberOfTestIterationsForTestWithSelector:(SEL)arg1; +- (void)afterTestIteration:(unsigned long long)arg1 selector:(SEL)arg2; +- (void)beforeTestIteration:(unsigned long long)arg1 selector:(SEL)arg2; +- (void)tearDownTestWithSelector:(SEL)arg1; +- (void)setUpTestWithSelector:(SEL)arg1; +- (void)performTest:(id)arg1; +- (void)invokeTest; +- (Class)testRunClass; +- (Class)_requiredTestRunBaseClass; +- (void)_recordUnexpectedFailureWithDescription:(id)arg1 error:(id)arg2; +- (void)_recordUnexpectedFailureWithDescription:(id)arg1 exception:(id)arg2; +// Exists since Xcode 9.4.1, at least +- (void)recordFailureWithDescription:(NSString *)arg1 inFile:(NSString *)arg2 atLine:(NSUInteger)arg3 expected:(BOOL)arg4; +- (void)_enqueueFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected; +- (void)_dequeueFailures; +- (void)_interruptTest; +- (BOOL)isEqual:(id)arg1; +- (id)nameForLegacyLogging; +- (id)name; +- (id)languageAgnosticTestMethodName; +- (unsigned long long)testCaseCount; +- (id)initWithSelector:(SEL)arg1; +- (id)init; +- (void)waiter:(id)arg1 didFulfillInvertedExpectation:(id)arg2; +- (void)waiter:(id)arg1 fulfillmentDidViolateOrderingConstraintsForExpectation:(id)arg2 requiredExpectation:(id)arg3; +- (void)waiter:(id)arg1 didTimeoutWithUnfulfilledExpectations:(id)arg2; +- (id)expectationForPredicate:(id)arg1 evaluatedWithObject:(id)arg2 handler:(CDUnknownBlockType)arg3; +- (id)expectationForNotification:(id)arg1 object:(id)arg2 handler:(CDUnknownBlockType)arg3; +- (id)keyValueObservingExpectationForObject:(id)arg1 keyPath:(id)arg2 handler:(CDUnknownBlockType)arg3; +- (id)keyValueObservingExpectationForObject:(id)arg1 keyPath:(id)arg2 expectedValue:(id)arg3; +- (void)_addExpectation:(id)arg1; +- (void)waitForExpectations:(id)arg1 timeout:(double)arg2 enforceOrder:(BOOL)arg3; +- (void)waitForExpectations:(id)arg1 timeout:(double)arg2; +- (void)waitForExpectationsWithTimeout:(double)arg1 handler:(CDUnknownBlockType)arg2; +- (void)_waitForExpectations:(id)arg1 timeout:(double)arg2 enforceOrder:(BOOL)arg3 handler:(CDUnknownBlockType)arg4; +- (id)expectationWithDescription:(id)arg1; +- (id)_expectationForDarwinNotification:(id)arg1; +- (void)nestedWaiter:(id)arg1 wasInterruptedByTimedOutWaiter:(id)arg2; + +@end diff --git a/PrivateHeaders/XCTest/XCTestCaseRun.h b/PrivateHeaders/XCTest/XCTestCaseRun.h new file mode 100644 index 0000000..fbd614f --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestCaseRun.h @@ -0,0 +1,18 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCTestCaseRun : XCTestRun +{ +} + +- (void)_recordValues:(id)arg1 forPerformanceMetricID:(id)arg2 name:(id)arg3 unitsOfMeasurement:(id)arg4 baselineName:(id)arg5 baselineAverage:(id)arg6 maxPercentRegression:(id)arg7 maxPercentRelativeStandardDeviation:(id)arg8 maxRegression:(id)arg9 maxStandardDeviation:(id)arg10 file:(id)arg11 line:(unsigned long long)arg12; +- (void)recordFailureInTest:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4 expected:(BOOL)arg5; +- (void)stop; +- (void)start; + +@end diff --git a/PrivateHeaders/XCTest/XCTestCaseSuite.h b/PrivateHeaders/XCTest/XCTestCaseSuite.h new file mode 100644 index 0000000..688fdbc --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestCaseSuite.h @@ -0,0 +1,19 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCTestCaseSuite : XCTestSuite +{ + Class _testCaseClass; +} + ++ (id)emptyTestSuiteForTestCaseClass:(Class)arg1; +- (void)tearDown; +- (void)setUp; +- (id)initWithTestCaseClass:(Class)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestConfiguration.h b/PrivateHeaders/XCTest/XCTestConfiguration.h new file mode 100644 index 0000000..22d96f1 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestConfiguration.h @@ -0,0 +1,66 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSSet, NSString, NSURL, NSUUID; + +@interface XCTestConfiguration : NSObject +{ + NSURL *_testBundleURL; + NSString *_testBundleRelativePath; + NSString *_absolutePath; + NSSet *_testsToSkip; + NSSet *_testsToRun; + BOOL _reportResultsToIDE; + NSUUID *_sessionIdentifier; + NSString *_pathToXcodeReportingSocket; + BOOL _disablePerformanceMetrics; + BOOL _treatMissingBaselinesAsFailures; + NSURL *_baselineFileURL; + NSString *_baselineFileRelativePath; + NSString *_targetApplicationPath; + NSString *_targetApplicationBundleID; + NSString *_productModuleName; + BOOL _reportActivities; + BOOL _testsMustRunOnMainThread; + BOOL _initializeForUITesting; + NSArray *_targetApplicationArguments; + NSDictionary *_targetApplicationEnvironment; + NSDictionary *_aggregateStatisticsBeforeCrash; + NSString *_automationFrameworkPath; + BOOL _emitOSLogs; +} +@property BOOL emitOSLogs; // @synthesize emitOSLogs=_emitOSLogs; +@property(copy) NSString *automationFrameworkPath; // @synthesize automationFrameworkPath=_automationFrameworkPath; +@property(copy) NSDictionary *aggregateStatisticsBeforeCrash; // @synthesize aggregateStatisticsBeforeCrash=_aggregateStatisticsBeforeCrash; +@property(copy) NSArray *targetApplicationArguments; // @synthesize targetApplicationArguments=_targetApplicationArguments; +@property(copy) NSDictionary *targetApplicationEnvironment; // @synthesize targetApplicationEnvironment=_targetApplicationEnvironment; +@property BOOL initializeForUITesting; // @synthesize initializeForUITesting=_initializeForUITesting; +@property BOOL testsMustRunOnMainThread; // @synthesize testsMustRunOnMainThread=_testsMustRunOnMainThread; +@property BOOL reportActivities; // @synthesize reportActivities=_reportActivities; +@property(copy) NSString *productModuleName; // @synthesize productModuleName=_productModuleName; +@property(copy) NSString *targetApplicationBundleID; // @synthesize targetApplicationBundleID=_targetApplicationBundleID; +@property(copy) NSString *targetApplicationPath; // @synthesize targetApplicationPath=_targetApplicationPath; +@property BOOL treatMissingBaselinesAsFailures; // @synthesize treatMissingBaselinesAsFailures=_treatMissingBaselinesAsFailures; +@property BOOL disablePerformanceMetrics; // @synthesize disablePerformanceMetrics=_disablePerformanceMetrics; +@property BOOL reportResultsToIDE; // @synthesize reportResultsToIDE=_reportResultsToIDE; +@property(copy, nonatomic) NSURL *baselineFileURL; // @synthesize baselineFileURL=_baselineFileURL; +@property(copy) NSString *baselineFileRelativePath; // @synthesize baselineFileRelativePath=_baselineFileRelativePath; +@property(copy) NSString *pathToXcodeReportingSocket; // @synthesize pathToXcodeReportingSocket=_pathToXcodeReportingSocket; +@property(copy) NSUUID *sessionIdentifier; // @synthesize sessionIdentifier=_sessionIdentifier; +@property(copy) NSSet *testsToSkip; // @synthesize testsToSkip=_testsToSkip; +@property(copy) NSSet *testsToRun; // @synthesize testsToRun=_testsToRun; +@property(copy, nonatomic) NSURL *testBundleURL; // @synthesize testBundleURL=_testBundleURL; +@property(copy) NSString *testBundleRelativePath; // @synthesize testBundleRelativePath=_testBundleRelativePath; +@property(copy) NSString *absolutePath; // @synthesize absolutePath=_absolutePath; + ++ (id)configurationWithContentsOfFile:(id)arg1; ++ (id)activeTestConfiguration; ++ (void)setActiveTestConfiguration:(id)arg1; + +- (BOOL)writeToFile:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTestContext.h b/PrivateHeaders/XCTest/XCTestContext.h new file mode 100644 index 0000000..7284860 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestContext.h @@ -0,0 +1,26 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, XCTestContextScope; + +@interface XCTestContext : NSObject +{ + BOOL _didHandleUIInterruption; + XCTestContextScope *_currentScope; +} +@property BOOL didHandleUIInterruption; // @synthesize didHandleUIInterruption=_didHandleUIInterruption; +@property(retain, nonatomic) XCTestContextScope *currentScope; // @synthesize currentScope=_currentScope; +@property(readonly, copy) NSArray *handlers; + ++ (CDUnknownBlockType)defaultAsynchronousUIElementHandler; + +- (BOOL)handleAsynchronousUIElement:(id)arg1; +- (void)removeUIInterruptionMonitor:(id)arg1; +- (id)addUIInterruptionMonitorWithDescription:(id)arg1 handler:(CDUnknownBlockType)arg2; +- (void)performInScope:(CDUnknownBlockType)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTestContextScope.h b/PrivateHeaders/XCTest/XCTestContextScope.h new file mode 100644 index 0000000..7fd8e86 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestContextScope.h @@ -0,0 +1,19 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableArray; + +@interface XCTestContextScope : NSObject +{ + XCTestContextScope *_parentScope; + NSMutableArray *_handlers; +} +@property(copy) NSMutableArray *handlers; // @synthesize handlers=_handlers; +@property(readonly) XCTestContextScope *parentScope; // @synthesize parentScope=_parentScope; + +- (id)initWithParentScope:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestDriver.h b/PrivateHeaders/XCTest/XCTestDriver.h new file mode 100644 index 0000000..6529c18 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestDriver.h @@ -0,0 +1,38 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import "XCDebugLogDelegate-Protocol.h" +#import "XCTestDriverInterface-Protocol.h" +#import "XCTestManager_TestsInterface-Protocol.h" +#import "XCTestManager_IDEInterface-Protocol.h" +#import "XCTestManager_ManagerInterface-Protocol.h" + +@class DTXConnection, NSMutableArray, NSString, NSUUID, NSXPCConnection, XCTestConfiguration, XCTestSuite; + +@interface XCTestDriver : NSObject +{ + XCTestConfiguration *_testConfiguration; + NSObject *_queue; + NSMutableArray *_debugMessageBuffer; + int _debugMessageBufferOverflow; +} +@property int debugMessageBufferOverflow; // @synthesize debugMessageBufferOverflow=_debugMessageBufferOverflow; +@property(retain) NSMutableArray *debugMessageBuffer; // @synthesize debugMessageBuffer=_debugMessageBuffer; +@property(retain) NSObject *queue; // @synthesize queue=_queue; +@property(readonly) XCTestConfiguration *testConfiguration; // @synthesize testConfiguration=_testConfiguration; + +- (void)runTestConfiguration:(id)arg1 completionHandler:(CDUnknownBlockType)arg2; +- (void)runTestSuite:(id)arg1 completionHandler:(CDUnknownBlockType)arg2; +- (void)reportStallOnMainThreadInTestCase:(id)arg1 method:(id)arg2 file:(id)arg3 line:(unsigned long long)arg4; +- (BOOL)runTestsAndReturnError:(id *)arg1; +- (id)_readyIDESession:(id *)arg1; +- (int)_connectedSocketForIDESession:(id *)arg1; +- (void)logDebugMessage:(id)arg1; +- (id)initWithTestConfiguration:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestDriverInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestDriverInterface-Protocol.h new file mode 100644 index 0000000..1108f8b --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestDriverInterface-Protocol.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSNumber; + +@protocol XCTestDriverInterface +- (id)_IDE_processWithToken:(NSNumber *)arg1 exitedWithStatus:(NSNumber *)arg2; +- (id)_IDE_stopTrackingProcessWithToken:(NSNumber *)arg1; +- (id)_IDE_processWithBundleID:(NSString *)arg1 path:(NSString *)arg2 pid:(NSNumber *)arg3 crashedUnderSymbol:(NSString *)arg4; +- (id)_IDE_startExecutingTestPlanWithProtocolVersion:(NSNumber *)arg1; +@end diff --git a/PrivateHeaders/XCTest/XCTestExpectation.h b/PrivateHeaders/XCTest/XCTestExpectation.h new file mode 100644 index 0000000..b4c3e8b --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestExpectation.h @@ -0,0 +1,39 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + + + +@class _XCTestExpectationImplementation; + +@interface XCTestExpectation : NSObject +{ + id _internalImplementation; +} +@property BOOL hasBeenWaitedOn; +@property id delegate; +@property(readonly, copy) NSArray *fulfillCallStackReturnAddresses; +@property(readonly) BOOL fulfilled; +@property BOOL hasInverseBehavior; +@property(getter=isInverted) BOOL inverted; +@property(nonatomic) BOOL assertForOverFulfill; +@property(nonatomic) unsigned long long expectedFulfillmentCount; +@property(nonatomic) unsigned long long fulfillmentCount; +@property(readonly) unsigned long long fulfillmentToken; +@property(readonly) _XCTestExpectationImplementation *internalImplementation; // @synthesize internalImplementation=_internalImplementation; +@property(copy) NSString *expectationDescription; +@property(readonly, nonatomic) NSObject *delegateQueue; +@property(readonly, nonatomic) NSObject *queue; + ++ (id)expectationWithDescription:(id)arg1; + +- (void)cleanup; +- (void)_queue_fulfillWithCallStackReturnAddresses:(id)arg1; +- (void)fulfill; + +- (id)initWithDescription:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTestExpectationDelegate-Protocol.h b/PrivateHeaders/XCTest/XCTestExpectationDelegate-Protocol.h new file mode 100644 index 0000000..4270ee1 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestExpectationDelegate-Protocol.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class XCTestExpectation; + +@protocol XCTestExpectationDelegate +- (void)didFulfillExpectation:(XCTestExpectation *)arg1; +@end + diff --git a/PrivateHeaders/XCTest/XCTestExpectationWaiter.h b/PrivateHeaders/XCTest/XCTestExpectationWaiter.h new file mode 100644 index 0000000..911cef3 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestExpectationWaiter.h @@ -0,0 +1,17 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCTestExpectationWaiter : XCTWaiter +{ +} + +- (long long)wait:(double)arg1 forExpectations:(id)arg2 enforceOrder:(BOOL)arg3; +- (long long)wait:(double)arg1 forExpectations:(id)arg2; + +@end + diff --git a/PrivateHeaders/XCTest/XCTestLog.h b/PrivateHeaders/XCTest/XCTestLog.h new file mode 100644 index 0000000..750681d --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestLog.h @@ -0,0 +1,32 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import "XCTestObservation.h" + +@class NSFileHandle, NSString; + +@interface XCTestLog : XCTestObserver +{ +} +@property(readonly) NSFileHandle *logFileHandle; + ++ (id)_messageForTest:(id)arg1 didMeasureValues:(id)arg2 forPerformanceMetricID:(id)arg3 name:(id)arg4 unitsOfMeasurement:(id)arg5 baselineName:(id)arg6 baselineAverage:(id)arg7 maxPercentRegression:(id)arg8 maxPercentRelativeStandardDeviation:(id)arg9 maxRegression:(id)arg10 maxStandardDeviation:(id)arg11 file:(id)arg12 line:(unsigned long long)arg13; + +- (void)testCaseDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)_testCase:(id)arg1 didMeasureValues:(id)arg2 forPerformanceMetricID:(id)arg3 name:(id)arg4 unitsOfMeasurement:(id)arg5 baselineName:(id)arg6 baselineAverage:(id)arg7 maxPercentRegression:(id)arg8 maxPercentRelativeStandardDeviation:(id)arg9 maxRegression:(id)arg10 maxStandardDeviation:(id)arg11 file:(id)arg12 line:(unsigned long long)arg13; +- (void)testCaseDidStop:(id)arg1; +- (void)testCaseDidStart:(id)arg1; +- (void)testSuiteDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testSuiteDidStop:(id)arg1; +- (void)testSuiteDidStart:(id)arg1; +- (void)_testDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testLogWithFormat:(id)arg1 arguments:(char *)arg2; +- (void)testLogWithFormat:(id)arg1; +- (id)dateFormatter; + +@end diff --git a/PrivateHeaders/XCTest/XCTestManager_IDEInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestManager_IDEInterface-Protocol.h new file mode 100644 index 0000000..ee1c77f --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestManager_IDEInterface-Protocol.h @@ -0,0 +1,36 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSDictionary, NSNumber, NSString, XCActivityRecord; + +@protocol XCTestManager_IDEInterface +- (id)_XCT_handleCrashReportData:(NSData *)arg1 fromFileWithName:(NSString *)arg2; +- (id)_XCT_nativeFocusItemDidChangeAtTime:(NSNumber *)arg1 parameterSnapshot:(id/*XCElementSnapshot*/)arg2 applicationSnapshot:(id/*XCElementSnapshot*/)arg3; +- (id)_XCT_recordedEventNames:(NSArray *)arg1 timestamp:(NSNumber *)arg2 duration:(NSNumber *)arg3 startLocation:(NSDictionary *)arg4 startElementSnapshot:(id/*XCElementSnapshot*/)arg5 startApplicationSnapshot:(id/*XCElementSnapshot*/)arg6 endLocation:(NSDictionary *)arg7 endElementSnapshot:(id/*XCElementSnapshot*/)arg8 endApplicationSnapshot:(id/*XCElementSnapshot*/)arg9; +- (id)_XCT_testCase:(NSString *)arg1 method:(NSString *)arg2 didFinishActivity:(XCActivityRecord *)arg3; +- (id)_XCT_testCase:(NSString *)arg1 method:(NSString *)arg2 willStartActivity:(XCActivityRecord *)arg3; +- (id)_XCT_recordedOrientationChange:(NSString *)arg1; +- (id)_XCT_recordedFirstResponderChangedWithApplicationSnapshot:(id/*XCElementSnapshot*/)arg1; +- (id)_XCT_exchangeCurrentProtocolVersion:(NSNumber *)arg1 minimumVersion:(NSNumber *)arg2; +- (id)_XCT_recordedKeyEventsWithApplicationSnapshot:(id/*XCElementSnapshot*/)arg1 characters:(NSString *)arg2 charactersIgnoringModifiers:(NSString *)arg3 modifierFlags:(NSNumber *)arg4; +- (id)_XCT_logDebugMessage:(NSString *)arg1; +- (id)_XCT_logMessage:(NSString *)arg1; +- (id)_XCT_testMethod:(NSString *)arg1 ofClass:(NSString *)arg2 didMeasureMetric:(NSDictionary *)arg3 file:(NSString *)arg4 line:(NSNumber *)arg5; +- (id)_XCT_testCase:(NSString *)arg1 method:(NSString *)arg2 didStallOnMainThreadInFile:(NSString *)arg3 line:(NSNumber *)arg4; +- (id)_XCT_testCaseDidFinishForTestClass:(NSString *)arg1 method:(NSString *)arg2 withStatus:(NSString *)arg3 duration:(NSNumber *)arg4; +- (id)_XCT_testCaseDidFailForTestClass:(NSString *)arg1 method:(NSString *)arg2 withMessage:(NSString *)arg3 file:(NSString *)arg4 line:(NSNumber *)arg5; +- (id)_XCT_testCaseDidStartForTestClass:(NSString *)arg1 method:(NSString *)arg2; +- (id)_XCT_testSuite:(NSString *)arg1 didFinishAt:(NSString *)arg2 runCount:(NSNumber *)arg3 withFailures:(NSNumber *)arg4 unexpected:(NSNumber *)arg5 testDuration:(NSNumber *)arg6 totalDuration:(NSNumber *)arg7; +- (id)_XCT_testSuite:(NSString *)arg1 didStartAt:(NSString *)arg2; +- (id)_XCT_initializationForUITestingDidFailWithError:(NSError *)arg1; +- (id)_XCT_didBeginInitializingForUITesting; +- (id)_XCT_didFinishExecutingTestPlan; +- (id)_XCT_didBeginExecutingTestPlan; +- (id)_XCT_testBundleReadyWithProtocolVersion:(NSNumber *)arg1 minimumVersion:(NSNumber *)arg2; +- (id)_XCT_getProgressForLaunch:(id)arg1; +- (id)_XCT_terminateProcess:(id)arg1; +- (id)_XCT_launchProcessWithPath:(NSString *)arg1 bundleID:(NSString *)arg2 arguments:(NSArray *)arg3 environmentVariables:(NSDictionary *)arg4; +@end diff --git a/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h new file mode 100644 index 0000000..d68f25c --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h @@ -0,0 +1,54 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSArray, NSDictionary, NSNumber, NSString, NSUUID, XCSynthesizedEventRecord, XCTouchGesture, NSXPCListenerEndpoint; + +@protocol XCTestManager_ManagerInterface +// since Xcode9 +- (void)_XCT_requestBundleIDForPID:(int)arg1 reply:(void (^)(NSString *, NSError *))arg2; +- (void)_XCT_loadAccessibilityWithTimeout:(double)arg1 reply:(void (^)(BOOL, NSError *))arg2; +- (void)_XCT_injectVoiceRecognitionAudioInputPaths:(NSArray *)arg1 completion:(void (^)(BOOL, NSError *))arg2; +- (void)_XCT_injectAssistantRecognitionStrings:(NSArray *)arg1 completion:(void (^)(BOOL, NSError *))arg2; +- (void)_XCT_startSiriUIRequestWithAudioFileURL:(NSURL *)arg1 completion:(void (^)(BOOL, NSError *))arg2; +- (void)_XCT_startSiriUIRequestWithText:(NSString *)arg1 completion:(void (^)(BOOL, NSError *))arg2; +- (void)_XCT_requestDTServiceHubConnectionWithReply:(void (^)(NSXPCListenerEndpoint *, NSError *))arg1; +- (void)_XCT_enableFauxCollectionViewCells:(void (^)(BOOL, NSError *))arg1; +- (void)_XCT_setAXTimeout:(double)arg1 reply:(void (^)(int))arg2; +- (void)_XCT_requestScreenshotWithReply:(void (^)(NSData *, NSError *))arg1; +- (void)_XCT_sendString:(NSString *)arg1 maximumFrequency:(NSUInteger)arg2 completion:(void (^)(NSError *))arg3; +- (void)_XCT_updateDeviceOrientation:(long long)arg1 completion:(void (^)(NSError *))arg2; +- (void)_XCT_performDeviceEvent:(id/*XCDeviceEvent*/)arg1 completion:(void (^)(NSError *))arg2; +- (void)_XCT_synthesizeEvent:(XCSynthesizedEventRecord *)arg1 completion:(void (^)(NSError *))arg2; +- (void)_XCT_requestElementAtPoint:(CGPoint)arg1 reply:(void (^)(id/*XCAccessibilityElement*/, NSError *))arg2; +- (void)_XCT_fetchParameterizedAttributeForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSNumber *)arg2 parameter:(id)arg3 reply:(void (^)(id, NSError *))arg4; +- (void)_XCT_setAttribute:(NSNumber *)arg1 value:(id)arg2 element:(id/*XCAccessibilityElement*/)arg3 reply:(void (^)(BOOL, NSError *))arg4; +- (void)_XCT_fetchAttributes:(id)attributes forElement:(id)element reply:(void (^)(NSDictionary *, NSError *))reply; +- (void)_XCT_fetchAttributesForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSArray *)arg2 reply:(void (^)(NSDictionary *, NSError *))arg3; +- (void)_XCT_terminateApplicationWithBundleID:(NSString *)arg1 completion:(void (^)(NSError *))arg2; +- (void)_XCT_performAccessibilityAction:(int)arg1 onElement:(id/*XCAccessibilityElement*/)arg2 withValue:(id)arg3 reply:(void (^)(NSError *))arg4; +- (void)_XCT_unregisterForAccessibilityNotification:(int)arg1 withRegistrationToken:(NSNumber *)arg2 reply:(void (^)(NSError *))arg3; +- (void)_XCT_registerForAccessibilityNotification:(int)arg1 reply:(void (^)(NSNumber *, NSError *))arg2; +- (void)_XCT_launchApplicationWithBundleID:(NSString *)arg1 arguments:(NSArray *)arg2 environment:(NSDictionary *)arg3 completion:(void (^)(NSError *))arg4; +- (void)_XCT_startMonitoringApplicationWithBundleID:(NSString *)arg1; +- (void)_XCT_requestBackgroundAssertionForPID:(int)arg1 reply:(void (^)(BOOL))arg2; +- (void)_XCT_requestBackgroundAssertionWithReply:(void (^)(void))arg1; +- (void)_XCT_registerTarget; +- (void)_XCT_requestEndpointForTestTargetWithPID:(int)arg1 preferredBackendPath:(NSString *)arg2 reply:(void (^)(NSXPCListenerEndpoint *, NSError *))arg3; +- (void)_XCT_requestSocketForSessionIdentifier:(NSUUID *)arg1 reply:(void (^)(NSFileHandle *))arg2; +- (void)_XCT_exchangeProtocolVersion:(unsigned long long)arg1 reply:(void (^)(unsigned long long))arg2; + +// Available since Xcode9 +// The first screenID type changed from "unsigned int" to "long long" since Xcode 13.3 in XCTAutomationSupport.framework/XCTScreenshotRequest.h +// but this place is still "unsigned int" in the header. Appium/WDA changes to "long long" for Xcode 13.3 x iOS 15.4 environment. +- (void)_XCT_requestScreenshotOfScreenWithID:(long long)arg1 withRect:(struct CGRect)arg2 uti:(NSString *)arg3 compressionQuality:(double)arg4 withReply:(void (^)(NSData *, NSError *))arg5; +- (void)_XCT_requestScreenshotOfScreenWithID:(long long)arg1 withRect:(struct CGRect)arg2 withReply:(void (^)(NSData *, NSError *))arg3; +- (void)_XCT_requestSnapshotForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSArray *)arg2 parameters:(NSDictionary *)arg3 reply:(void (^)(id/*XCElementSnapshot*/, NSError *))arg4; + +// Available since Xcode 12.5. Required (!!!) to use since Xcode 13 +- (void)_XCT_requestScreenshot:(/*XCTScreenshotRequest * */id)arg1 withReply:(void (^)(/*XCTImage * */id, NSError *))arg2; +@end diff --git a/PrivateHeaders/XCTest/XCTestManager_TestsInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestManager_TestsInterface-Protocol.h new file mode 100644 index 0000000..f4d7726 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestManager_TestsInterface-Protocol.h @@ -0,0 +1,12 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSData, NSString; + +@protocol XCTestManager_TestsInterface +- (void)_XCT_receivedAccessibilityNotification:(int)arg1 withPayload:(NSData *)arg2; +- (void)_XCT_applicationWithBundleID:(NSString *)arg1 didUpdatePID:(int)arg2 andState:(unsigned long long)arg3; +@end diff --git a/PrivateHeaders/XCTest/XCTestMisuseObserver.h b/PrivateHeaders/XCTest/XCTestMisuseObserver.h new file mode 100644 index 0000000..5bab99c --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestMisuseObserver.h @@ -0,0 +1,37 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +#import "XCTestObservation.h" + +@class NSMutableArray, NSString, XCTestCase, XCTestSuite; + +@interface XCTestMisuseObserver : NSObject +{ + CDUnknownBlockType _warningLogHandler; + NSMutableArray *_testSuiteStack; + XCTestCase *_currentTestCase; +} +@property(retain) XCTestCase *currentTestCase; // @synthesize currentTestCase=_currentTestCase; +@property(readonly) NSMutableArray *testSuiteStack; // @synthesize testSuiteStack=_testSuiteStack; +@property(readonly, copy) CDUnknownBlockType warningLogHandler; // @synthesize warningLogHandler=_warningLogHandler; +@property(readonly) XCTestSuite *currentTestSuite; + +- (void)testCaseDidFinish:(id)arg1; +- (void)testCase:(id)arg1 didFailWithDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testCaseWillStart:(id)arg1; +- (void)testSuiteDidFinish:(id)arg1; +- (void)testSuiteWillStart:(id)arg1; +- (BOOL)testSuiteStackContainsTestSuite:(id)arg1; +- (void)removeTestSuiteFromStack:(id)arg1; +- (void)popCurrentTestSuite; +- (void)pushTestSuite:(id)arg1; +- (void)emitWarningLog:(id)arg1; + +- (id)initWithWarningLogHandler:(CDUnknownBlockType)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestObservation-Protocol.h b/PrivateHeaders/XCTest/XCTestObservation-Protocol.h new file mode 100644 index 0000000..fa328ed --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestObservation-Protocol.h @@ -0,0 +1,20 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSBundle, NSString, XCTestCase, XCTestSuite; + +@protocol XCTestObservation + +@optional +- (void)testCaseDidFinish:(XCTestCase *)arg1; +- (void)testCase:(XCTestCase *)arg1 didFailWithDescription:(NSString *)arg2 inFile:(NSString *)arg3 atLine:(unsigned long long)arg4; +- (void)testCaseWillStart:(XCTestCase *)arg1; +- (void)testSuiteDidFinish:(XCTestSuite *)arg1; +- (void)testSuite:(XCTestSuite *)arg1 didFailWithDescription:(NSString *)arg2 inFile:(NSString *)arg3 atLine:(unsigned long long)arg4; +- (void)testSuiteWillStart:(XCTestSuite *)arg1; +- (void)testBundleDidFinish:(NSBundle *)arg1; +- (void)testBundleWillStart:(NSBundle *)arg1; +@end diff --git a/PrivateHeaders/XCTest/XCTestObservationCenter.h b/PrivateHeaders/XCTest/XCTestObservationCenter.h new file mode 100644 index 0000000..30301fa --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestObservationCenter.h @@ -0,0 +1,36 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableSet; + +@interface XCTestObservationCenter : NSObject +{ + id _internalImplementation; +} +@property BOOL suspended; +@property(readonly) NSMutableSet *observers; + ++ (id)sharedTestObservationCenter; + +- (void)_testCase:(id)arg1 didFinishActivity:(id)arg2; +- (void)_testCase:(id)arg1 willStartActivity:(id)arg2; +- (void)_testCaseDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)_testCase:(id)arg1 didMeasureValues:(id)arg2 forPerformanceMetricID:(id)arg3 name:(id)arg4 unitsOfMeasurement:(id)arg5 baselineName:(id)arg6 baselineAverage:(id)arg7 maxPercentRegression:(id)arg8 maxPercentRelativeStandardDeviation:(id)arg9 maxRegression:(id)arg10 maxStandardDeviation:(id)arg11 file:(id)arg12 line:(unsigned long long)arg13; +- (void)_testCaseDidStop:(id)arg1; +- (void)_testCaseDidStart:(id)arg1; +- (void)_testSuiteDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)_testSuiteDidStop:(id)arg1; +- (void)_testSuiteDidStart:(id)arg1; +- (void)_suspendObservationForBlock:(CDUnknownBlockType)arg1; +- (void)_suspendObservation; +- (void)_resumeObservation; +- (void)_observeTestExecutionForBlock:(CDUnknownBlockType)arg1; +- (void)removeTestObserver:(id)arg1; +- (void)addTestObserver:(id)arg1; +- (void)_addLegacyTestObserver:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCTestObserver.h b/PrivateHeaders/XCTest/XCTestObserver.h new file mode 100644 index 0000000..267e2f4 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestObserver.h @@ -0,0 +1,20 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface XCTestObserver : NSObject +{ +} + +- (void)testCaseDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testCaseDidStop:(id)arg1; +- (void)testCaseDidStart:(id)arg1; +- (void)testSuiteDidFail:(id)arg1 withDescription:(id)arg2 inFile:(id)arg3 atLine:(unsigned long long)arg4; +- (void)testSuiteDidStop:(id)arg1; +- (void)testSuiteDidStart:(id)arg1; +- (void)stopObserving; +- (void)startObserving; + +@end diff --git a/PrivateHeaders/XCTest/XCTestProbe.h b/PrivateHeaders/XCTest/XCTestProbe.h new file mode 100644 index 0000000..de6058c --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestProbe.h @@ -0,0 +1,13 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface XCTestProbe : NSObject +{ +} + ++ (BOOL)isTesting; + +@end diff --git a/PrivateHeaders/XCTest/XCTestRun.h b/PrivateHeaders/XCTest/XCTestRun.h new file mode 100644 index 0000000..8197c6a --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestRun.h @@ -0,0 +1,39 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSDate, XCTest, _XCInternalTestRun; + +@interface XCTestRun () +{ + id _internalTestRun; +} +@property(readonly) _XCInternalTestRun *implementation; // @synthesize implementation=_internalTestRun; +@property(readonly) BOOL hasSucceeded; +@property unsigned long long unexpectedExceptionCountBeforeCrash; +@property unsigned long long failureCountBeforeCrash; +@property unsigned long long executionCountBeforeCrash; +@property(readonly) unsigned long long testCaseCount; +@property(readonly) unsigned long long unexpectedExceptionCount; +@property(readonly) unsigned long long failureCount; +@property(readonly) unsigned long long totalFailureCount; +@property(readonly) unsigned long long executionCount; +@property(readonly, copy) NSDate *stopDate; +@property(readonly, copy) NSDate *startDate; +@property(readonly) double testDuration; +@property(readonly) double totalDuration; +@property(readonly) XCTest *test; + ++ (id)testRunWithTest:(id)arg1; + +- (void)recordFailureWithDescription:(id)arg1 inFile:(id)arg2 atLine:(unsigned long long)arg3 expected:(BOOL)arg4; +- (void)stop; +- (void)start; +- (id)init; +- (id)initWithTest:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestSuite.h b/PrivateHeaders/XCTest/XCTestSuite.h new file mode 100644 index 0000000..08c10ad --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestSuite.h @@ -0,0 +1,48 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSArray, NSMutableArray, NSString; + +@interface XCTestSuite : XCTest +{ + id _internalImplementation; +} +@property(readonly, copy) NSArray *tests; +@property(copy) NSString *name; + ++ (id)testSuiteForTestConfiguration:(id)arg1; ++ (id)defaultTestSuite; ++ (id)allTests; ++ (id)testSuiteForTestCaseClass:(Class)arg1; ++ (id)testSuiteForTestCaseWithName:(id)arg1; ++ (id)testSuiteForBundlePath:(id)arg1; ++ (id)suiteForBundleCache; ++ (void)invalidateCache; ++ (id)_suiteForBundleCache; ++ (id)emptyTestSuiteNamedFromPath:(id)arg1; ++ (id)testSuiteWithName:(id)arg1; ++ (id)testCaseNamesForScopeNames:(id)arg1; + +- (id)_initWithTestConfiguration:(id)arg1; +- (void)_sortTestsUsingComparator:(CDUnknownBlockType)arg1; +- (void)performTest:(id)arg1; +- (void)_performProtectedSectionForTest:(id)arg1 testSection:(CDUnknownBlockType)arg2; +- (void)_recordUnexpectedFailureForTestRun:(id)arg1 description:(id)arg2 exception:(id)arg3; +- (void)recordFailureWithDescription:(id)arg1 inFile:(id)arg2 atLine:(unsigned long long)arg3 expected:(BOOL)arg4; +- (Class)testRunClass; +- (Class)_requiredTestRunBaseClass; +- (unsigned long long)testCaseCount; +- (void)setTests:(id)arg1; +- (void)addTest:(id)arg1; +- (id)_testSuiteWithIdentifier:(id)arg1; +- (id)description; +- (id)initWithName:(id)arg1; +- (id)init; +- (void)removeTestsWithNames:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestSuiteRun.h b/PrivateHeaders/XCTest/XCTestSuiteRun.h new file mode 100644 index 0000000..6a1790a --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestSuiteRun.h @@ -0,0 +1,28 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSArray, NSMutableArray; + +@interface XCTestSuiteRun : XCTestRun +{ + NSMutableArray *_testRuns; +} +@property(readonly, copy) NSArray *testRuns; + +- (void)recordFailureWithDescription:(id)arg1 inFile:(id)arg2 atLine:(unsigned long long)arg3 expected:(BOOL)arg4; +- (double)testDuration; +- (unsigned long long)unexpectedExceptionCount; +- (unsigned long long)failureCount; +- (unsigned long long)executionCount; +- (void)addTestRun:(id)arg1; +- (void)stop; +- (void)start; + +- (id)initWithTest:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCTestWaiter.h b/PrivateHeaders/XCTest/XCTestWaiter.h new file mode 100644 index 0000000..73289b0 --- /dev/null +++ b/PrivateHeaders/XCTest/XCTestWaiter.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCTestWaiter : XCTWaiter +{ +} + +@end + diff --git a/PrivateHeaders/XCTest/XCUIApplication.h b/PrivateHeaders/XCTest/XCUIApplication.h new file mode 100644 index 0000000..e092f26 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIApplication.h @@ -0,0 +1,60 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSArray, NSDictionary, NSString, XCApplicationQuery, XCUIApplicationImpl; + +@interface XCUIApplication () +{ + BOOL _ancillary; + BOOL _doesNotHandleUIInterruptions; + BOOL _idleAnimationWaitEnabled; + XCUIElement *_keyboard; + NSArray *_launchArguments; + NSDictionary *_launchEnvironment; + XCUIApplicationImpl *_applicationImpl; + XCApplicationQuery *_applicationQuery; + unsigned long long _generation; +} +@property unsigned long long generation; // @synthesize generation=_generation; +@property(retain) XCApplicationQuery *applicationQuery; // @synthesize applicationQuery=_applicationQuery; +@property(retain) XCUIApplicationImpl *applicationImpl; // @synthesize applicationQuery=_applicationQuery; +@property(readonly, copy) NSString *bundleID; // @synthesize bundleID=_bundleID; +@property(readonly, copy) NSString *path; // @synthesize path=_path; +@property BOOL ancillary; // @synthesize ancillary=_ancillary; +@property(readonly) XCUIElement *keyboard; // @synthesize keyboard=_keyboard; + +@property(getter=isIdleAnimationWaitEnabled) BOOL idleAnimationWaitEnabled; // @synthesize idleAnimationWaitEnabled=_idleAnimationWaitEnabled; +@property(nonatomic) BOOL doesNotHandleUIInterruptions; // @synthesize doesNotHandleUIInterruptions=_doesNotHandleUIInterruptions; +@property(readonly) BOOL fauxCollectionViewCellsEnabled; +#if !TARGET_OS_TV +@property(readonly, nonatomic) UIInterfaceOrientation interfaceOrientation; //TODO tvos +#endif +@property(readonly, nonatomic) BOOL running; +@property(nonatomic) pid_t processID; // @synthesize processID=_processID; +@property(readonly) id/*XCAccessibilityElement*/ accessibilityElement; + ++ (instancetype)applicationWithPID:(pid_t)processID; +- (void)activate; + +- (void)dismissKeyboard; +- (BOOL)setFauxCollectionViewCellsEnabled:(BOOL)arg1 error:(id *)arg2; +- (void)_waitForViewControllerViewDidDisappearWithTimeout:(double)arg1; +- (void)_waitForQuiescence; +- (void)terminate; +- (void)_launchUsingXcode:(BOOL)arg1; +- (void)launch; +- (id)application; +- (id)description; +- (id)lastSnapshot; +- (XCUIElementQuery *)query; +- (void)clearQuery; +- (void)resolveHandleUIInterruption:(BOOL)arg1; +- (id)initPrivateWithPath:(id)arg1 bundleID:(id)arg2; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/XCUIApplicationImpl.h b/PrivateHeaders/XCTest/XCUIApplicationImpl.h new file mode 100644 index 0000000..78de542 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIApplicationImpl.h @@ -0,0 +1,38 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSString, XCUIApplicationProcess; + +@interface XCUIApplicationImpl : NSObject +{ + NSString *_path; + NSString *_bundleID; + XCUIApplicationProcess *_currentProcess; +} + +@property(retain, nonatomic) XCUIApplicationProcess *currentProcess; // @synthesize currentProcess=_currentProcess; +@property(readonly, copy) NSString *bundleID; // @synthesize bundleID=_bundleID; +@property(readonly, copy) NSString *path; // @synthesize path=_path; +@property(nonatomic) unsigned long long state; +@property(nonatomic) int processID; +@property(readonly) id/*XCAccessibilityElement*/ accessibilityElement; + +- (instancetype)initWithPath:(id)arg1 bundleID:(id)arg2; + +- (void)launchWithArguments:(id)arg1 environment:(id)arg2 usingXcode:(BOOL)arg3; +- (void)handleCrashUnderSymbol:(id)arg1; +- (void)terminate; + +- (void)waitForViewControllerViewDidDisappearWithTimeout:(double)arg1; + +- (void)_waitForRunningActive; +- (void)_launchUsingPlatformWithArguments:(id)arg1 environment:(id)arg2; +- (void)_launchUsingXcodeWithArguments:(id)arg1 environment:(id)arg2; +- (void)_waitForLaunchProgress; + +@end diff --git a/PrivateHeaders/XCTest/XCUIApplicationProcess.h b/PrivateHeaders/XCTest/XCUIApplicationProcess.h new file mode 100644 index 0000000..6477039 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIApplicationProcess.h @@ -0,0 +1,85 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +#import + +@class XCAXClient_iOS; +@class XCApplicationMonitor; +@class XCUIApplicationImpl; +@protocol XCTestManager_IDEInterface; +@protocol XCTRunnerAutomationSession; + +@interface XCUIApplicationProcess : NSObject +{ + NSObject *_queue; + BOOL _accessibilityActive; + unsigned long long _applicationState; + int _processID; + id _token; + int _exitCode; + BOOL _eventLoopHasIdled; + BOOL _hasReceivedEventLoopHasIdled; + BOOL _animationsHaveFinished; + BOOL _hasReceivedAnimationsHaveFinished; + BOOL _hasExitCode; + BOOL _hasCrashReport; + NSString *_bundleID; + XCUIApplicationImpl *_applicationImplementation; + id _automationSession; + id/*XCElementSnapshot*/ _lastSnapshot; + XCApplicationMonitor *_applicationMonitor; + XCAXClient_iOS *_AXClient_iOS; +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(id)arg1; +@property XCAXClient_iOS *AXClient_iOS; // @synthesize AXClient_iOS=_AXClient_iOS; +// Since Xcode 10 +@property(retain) id/*XCElementSnapshot*/ lastSnapshot; // @synthesize lastSnapshot=_lastSnapshot; +@property XCApplicationMonitor *applicationMonitor; // @synthesize applicationMonitor=_applicationMonitor; +@property(retain) id automationSession; // @synthesize automationSession=_automationSession; +@property BOOL hasCrashReport; // @synthesize hasCrashReport=_hasCrashReport; +@property BOOL hasExitCode; // @synthesize hasExitCode=_hasExitCode; +@property BOOL hasReceivedAnimationsHaveFinished; +@property BOOL animationsHaveFinished; +@property BOOL hasReceivedEventLoopHasIdled; +@property BOOL eventLoopHasIdled; +@property int exitCode; +@property(retain) id token; +@property(nonatomic) int processID; +// Since Xcode 10.2 +@property(readonly, copy, nonatomic) NSString *bundleID; // @synthesize bundleID=_bundleID; +@property(readonly) BOOL running; +@property XCUIApplicationImpl *applicationImplementation; // @synthesize applicationImplementation=_applicationImplementation; +@property(nonatomic) unsigned long long applicationState; +@property(nonatomic) BOOL accessibilityActive; +@property(readonly, copy) id/*XCAccessibilityElement*/ accessibilityElement; + +- (id)init; +- (id)initWithApplicationMonitor:(id)arg1 AXInterface:(id)arg2; + +- (void)terminate; +- (void)waitForViewControllerViewDidDisappearWithTimeout:(double)arg1; +- (void)waitForAutomationSession; +// Before Xcode16-beta5 +- (void)waitForQuiescenceIncludingAnimationsIdle:(BOOL)arg1; +// Since Xcode16-beta5 +- (void)waitForQuiescenceIncludingAnimationsIdle:(BOOL)arg1 isPreEvent:(BOOL)arg2; + + +- (id)shortDescription; +- (id)_queue_description; + +// Gone with iOS 10.3 +- (void)waitForQuiescence; + +// Since Xcode 10.2 +- (void)_notifyWhenAnimationsAreIdle:(void (^)(id, void *))arg1; +- (_Bool)_supportsAnimationsIdleNotifications; +- (void)_notifyWhenMainRunLoopIsIdle:(void (^)(id, void *))arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCUICoordinate.h b/PrivateHeaders/XCTest/XCUICoordinate.h new file mode 100644 index 0000000..a7f7122 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUICoordinate.h @@ -0,0 +1,44 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import +#import + +@class XCUIElement; + +#if !TARGET_OS_TV +@interface XCUICoordinate () +{ + XCUIElement *_element; + XCUICoordinate *_coordinate; + CGVector _normalizedOffset; + CGVector _pointsOffset; +} + +@property(readonly) CGVector pointsOffset; // @synthesize pointsOffset=_pointsOffset; +@property(readonly) CGVector normalizedOffset; // @synthesize normalizedOffset=_normalizedOffset; +@property(readonly) XCUICoordinate *coordinate; // @synthesize coordinate=_coordinate; +@property(readonly) XCUIElement *element; // @synthesize element=_element; + +- (id)initWithCoordinate:(id)arg1 pointsOffset:(CGVector)arg2; +- (id)initWithElement:(id)arg1 normalizedOffset:(CGVector)arg2; +- (id)init; + +- (void)pressForDuration:(double)arg1 thenDragToCoordinate:(id)arg2; +- (void)pressForDuration:(double)arg1; +- (void)doubleTap; +- (void)tap; +- (void)pressWithPressure:(double)arg1 duration:(double)arg2; +- (void)forcePress; + +// Since Xcode 12 +- (void)pressForDuration:(double)duration + thenDragToCoordinate:(XCUICoordinate *)otherCoordinate + withVelocity:(CGFloat)velocity + thenHoldForDuration:(double)holdDuration; + +@end +#endif diff --git a/PrivateHeaders/XCTest/XCUIDevice.h b/PrivateHeaders/XCTest/XCUIDevice.h new file mode 100644 index 0000000..0d4dce2 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIDevice.h @@ -0,0 +1,30 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@interface XCUIDevice () + +// Since Xcode 10.2 +@property (readonly) id accessibilityInterface; // implements XCUIAccessibilityInterface +@property (readonly) id eventSynthesizer; // implements XCUIEventSynthesizing +@property (readonly) id screenDataSource; // @synthesize screenDataSource=_screenDataSource; + +- (_Bool)performDeviceEvent:(id)arg1 error:(id *)arg2; + +// Since Xcode 13 +// 1 - Light +// 2 - Dark +- (void)setAppearanceMode:(long long)arg1; +- (long long)appearanceMode; + +- (void)pressLockButton; +- (void)holdHomeButtonForDuration:(double)arg1; +- (void)_silentPressButton:(long long)arg1; +// Since Xcode 11 +- (_Bool)supportsPressureInteraction; + +@end diff --git a/PrivateHeaders/XCTest/XCUIElement.h b/PrivateHeaders/XCTest/XCUIElement.h new file mode 100644 index 0000000..d5dc188 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIElement.h @@ -0,0 +1,74 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSString, XCUIApplication, XCUICoordinate, XCUIElementQuery; + +@interface XCUIElement () +{ + BOOL _safeQueryResolutionEnabled; + XCUIElementQuery *_query; + id/*XCElementSnapshot*/ _lastSnapshot; +} + +@property BOOL safeQueryResolutionEnabled; // @synthesize safeQueryResolutionEnabled=_safeQueryResolutionEnabled; +@property(retain) id/*XCElementSnapshot*/ lastSnapshot; // @synthesize lastSnapshot=_lastSnapshot; +@property(readonly) XCUIElementQuery *query; // @synthesize query=_query; +#if !TARGET_OS_TV +@property(readonly, nonatomic) UIInterfaceOrientation interfaceOrientation; +#endif +@property(readonly, copy) XCUICoordinate *hitPointCoordinate; +@property(readonly) BOOL isTopLevelTouchBarElement; +@property(readonly) BOOL isTouchBarElement; +@property(readonly) BOOL hasKeyboardFocus; +@property(readonly, nonatomic) XCUIApplication *application; +// Added since Xcode 11.0 (beta) +@property(readonly, copy) XCUIElement *excludingNonModalElements; +// Added since Xcode 11.0 (GM) +@property(readonly, copy) XCUIElement *includingNonModalElements; + +- (id)initWithElementQuery:(id)arg1; + +- (unsigned long long)traits; +- (void)resolveHandleUIInterruption:(BOOL)arg1; +- (BOOL)waitForExistenceWithTimeout:(double)arg1; +- (BOOL)_waitForExistenceWithTimeout:(double)arg1; +- (id)_hitPointByAttemptingToScrollToVisibleSnapshot:(id)arg1 error:(id *)arg2; +- (BOOL)evaluatePredicateForExpectation:(id)arg1 debugMessage:(id *)arg2; +- (void)_swipe:(unsigned long long)arg1; +- (void)_tapWithNumberOfTaps:(unsigned long long)arg1 numberOfTouches:(unsigned long long)arg2 activityTitle:(id)arg3; +- (id)_highestNonWindowAncestorOfElement:(id)arg1 notSharedWithElement:(id)arg2; +- (id)_pointsInFrame:(CGRect)arg1 numberOfTouches:(unsigned long long)arg2; +// Since 11.3 +- (void)pressWithPressure:(double)arg1 duration:(double)arg2; +- (void)forcePress; +- (void)tapWithNumberOfTaps:(unsigned long long)arg1 numberOfTouches:(unsigned long long)arg2; +- (void)twoFingerTap; +- (void)doubleTap; +- (void)tap; +- (void)pressForDuration:(double)arg1 thenDragToElement:(id)arg2; +- (void)pressForDuration:(double)arg1; + +// Available since Xcode 11.0 +- (_Bool)resolveOrRaiseTestFailure:(_Bool)arg1 error:(id *)arg2; +- (void)resolveOrRaiseTestFailure; +// Available since Xcode 10.0 +- (id)screenshot; + +// Since Xcode 11.4 +- (void)swipeRightWithVelocity:(double)arg1; +- (void)swipeLeftWithVelocity:(double)arg1; +- (void)swipeDownWithVelocity:(double)arg1; +- (void)swipeUpWithVelocity:(double)arg1; + +// Since Xcode 12 +- (void)pressForDuration:(double)duration + thenDragToElement:(XCUIElement *)otherElement + withVelocity:(CGFloat)velocity + thenHoldForDuration:(double)holdDuration; + +@end diff --git a/PrivateHeaders/XCTest/XCUIElementAsynchronousHandlerWrapper.h b/PrivateHeaders/XCTest/XCUIElementAsynchronousHandlerWrapper.h new file mode 100644 index 0000000..78b6862 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIElementAsynchronousHandlerWrapper.h @@ -0,0 +1,19 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString, NSUUID; + +@interface XCUIElementAsynchronousHandlerWrapper : NSObject +{ + CDUnknownBlockType _handler; + NSString *_handlerDescription; + NSUUID *_identifier; +} +@property(copy) NSUUID *identifier; // @synthesize identifier=_identifier; +@property(copy) NSString *handlerDescription; // @synthesize handlerDescription=_handlerDescription; +@property(copy) CDUnknownBlockType handler; // @synthesize handler=_handler; + +@end diff --git a/PrivateHeaders/XCTest/XCUIElementHitPointCoordinate.h b/PrivateHeaders/XCTest/XCUIElementHitPointCoordinate.h new file mode 100644 index 0000000..75e3ae1 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIElementHitPointCoordinate.h @@ -0,0 +1,24 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import +#import + +#if !TARGET_OS_IPHONE + +@interface XCUIElementHitPointCoordinate : XCUICoordinate +{ +} + +- (id)description; +- (struct CGPoint)screenPoint; +- (id)initWithCoordinate:(id)arg1 pointsOffset:(struct CGVector)arg2; +- (id)initWithElement:(id)arg1 normalizedOffset:(struct CGVector)arg2; +- (id)initWithElement:(id)arg1; + +@end + +#endif diff --git a/PrivateHeaders/XCTest/XCUIElementQuery.h b/PrivateHeaders/XCTest/XCUIElementQuery.h new file mode 100644 index 0000000..b13f8b0 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIElementQuery.h @@ -0,0 +1,70 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import +#import +#import "XCTElementSetTransformer-Protocol.h" + +@class NSArray, NSOrderedSet, NSString, XCUIApplication, XCUIElement; + +@interface XCUIElementQuery () +{ + BOOL _changesScope; + NSString *_queryDescription; + XCUIElementQuery *_inputQuery; + CDUnknownBlockType _filter; + unsigned long long _expressedType; + NSArray *_expressedIdentifiers; + NSOrderedSet *_lastInput; + NSOrderedSet *_lastOutput; + id/*XCElementSnapshot*/ _rootElementSnapshot; + // Added since Xcode 11.0 (beta) + BOOL _modalViewPruningDisabled; +} + +@property(copy) NSOrderedSet *lastOutput; // @synthesize lastOutput=_lastOutput; +@property(copy) NSOrderedSet *lastInput; // @synthesize lastInput=_lastInput; +@property(copy) NSArray *expressedIdentifiers; // @synthesize expressedIdentifiers=_expressedIdentifiers; +@property unsigned long long expressedType; // @synthesize expressedType=_expressedType; +@property BOOL changesScope; // @synthesize changesScope=_changesScope; +@property(readonly, copy) CDUnknownBlockType filter; // @synthesize filter=_filter; +// Added since Xcode 11.0 (beta) +@property BOOL modalViewPruningDisabled; // @synthesize modalViewPruningDisabled=_modalViewPruningDisabled; +@property(readonly) XCUIElementQuery *inputQuery; // @synthesize inputQuery=_inputQuery; +@property(readonly, copy) NSString *queryDescription; // @synthesize queryDescription=_queryDescription; +@property(readonly, copy) NSString *elementDescription; +@property(readonly) XCUIApplication *application; +@property(retain) id/*XCElementSnapshot*/ rootElementSnapshot; // @synthesize rootElementSnapshot=_rootElementSnapshot; +@property(retain) NSObject *transformer; // @synthesize transformer = _transformer; + +// Added since Xcode 11.0 (beta) +@property(readonly, copy) XCUIElementQuery *excludingNonModalElements; +// Added since Xcode 11.0 (GM) +@property(readonly, copy) XCUIElementQuery *includingNonModalElements; + +- (id)matchingSnapshotsWithError:(id *)arg1; +- (id)matchingSnapshotsHandleUIInterruption:(BOOL)arg1 withError:(id *)arg2; +- (id)_elementMatchingAccessibilityElementOfSnapshot:(id)arg1; +- (id)_containingPredicate:(id)arg1 queryDescription:(id)arg2; +- (id)_predicateWithType:(unsigned long long)arg1 identifier:(id)arg2; +- (id)_queryWithPredicate:(id)arg1; +- (id)sorted:(CDUnknownBlockType)arg1; +- (id)descending:(unsigned long long)arg1; +- (id)ascending:(unsigned long long)arg1; +- (id)filter:(CDUnknownBlockType)arg1; +- (id)_debugInfoWithIndent:(id *)arg1; +- (id)_derivedExpressedIdentifiers; +- (unsigned long long)_derivedExpressedType; +- (id)initWithInputQuery:(id)arg1 queryDescription:(id)arg2 filter:(CDUnknownBlockType)arg3; + +// Added since Xcode 11.0 +- (id/*XCElementSnapshot*/)elementSnapshotForDebugDescriptionWithNoMatchesMessage:(id *)arg1; +// Added since Xcode 11.0 +- (id/*XCElementSnapshot*/)uniqueMatchingSnapshotWithError:(NSError **)arg1; +/*! DO NOT USE DIRECTLY! Please use fb_firstMatch instead */ +- (XCUIElement *)firstMatch; + +@end diff --git a/PrivateHeaders/XCTest/XCUIHitPointResult.h b/PrivateHeaders/XCTest/XCUIHitPointResult.h new file mode 100644 index 0000000..2cd03cc --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIHitPointResult.h @@ -0,0 +1,20 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import +#import + +@interface XCUIHitPointResult : NSObject +{ + BOOL _hittable; + CGPoint _hitPoint; +} + +@property(readonly, getter=isHittable) BOOL hittable; // @synthesize hittable=_hittable; +@property(readonly) struct CGPoint hitPoint; // @synthesize hitPoint=_hitPoint; +- (id)initWithHitPoint:(struct CGPoint)arg1 hittable:(BOOL)arg2; + +@end diff --git a/PrivateHeaders/XCTest/XCUIRecorderNodeFinder.h b/PrivateHeaders/XCTest/XCUIRecorderNodeFinder.h new file mode 100644 index 0000000..bf2d81d --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIRecorderNodeFinder.h @@ -0,0 +1,65 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSMutableArray, NSSet, XCUIRecorderNodeFinderMatch; + +@interface XCUIRecorderNodeFinder : NSObject +{ + unsigned long long _state; + NSSet *_descendantsWithTargetElementType; + NSArray *_childrenWithTargetElementType; + BOOL _allowDirectChildrenMatches; + BOOL _shouldAttemptToUseIdentifier; + BOOL _shouldAttemptToUsePlaceholderValue; + BOOL _shouldAttemptToUseLabel; + BOOL _shouldAttemptToUseTitle; + BOOL _shouldAttemptToUseTruncatedValueString; + BOOL _allowElementQueries; + BOOL _excludeUnlessNecessary; + NSMutableArray *_mutableFoundNodeMatches; + NSMutableArray *_unprocessedContainsMatches; + XCUIRecorderNodeFinderMatch *_ancestorNodeFinderMatch; + unsigned long long _targetSnapshotIndex; + id/*XCElementSnapshot*/ _targetSnapshot; + unsigned long long _language; + unsigned long long _platform; +} + ++ (id)nodeToFindElementForSnapshots:(id)arg1 language:(unsigned long long)arg2 platform:(unsigned long long)arg3; ++ (id)_nodeFindersForSnapshots:(id)arg1 ancestorMatch:(id)arg2 ancestorIndex:(unsigned long long)arg3 stopCombinatorialExpansionIndexes:(id)arg4 excludeUnlessNecessaryElementTypes:(id)arg5 language:(unsigned long long)arg6 platform:(unsigned long long)arg7; ++ (id)_excludeUnlessNecessaryElementTypesForPlatform:(unsigned long long)arg1; ++ (id)_stopCombinatorialExpansionElementTypesForPlatform:(unsigned long long)arg1; +@property BOOL excludeUnlessNecessary; // @synthesize excludeUnlessNecessary=_excludeUnlessNecessary; +@property BOOL allowElementQueries; // @synthesize allowElementQueries=_allowElementQueries; +@property unsigned long long platform; // @synthesize platform=_platform; +@property unsigned long long language; // @synthesize language=_language; +@property(retain) id/*XCElementSnapshot*/ targetSnapshot; // @synthesize targetSnapshot=_targetSnapshot; +@property unsigned long long targetSnapshotIndex; // @synthesize targetSnapshotIndex=_targetSnapshotIndex; +@property(retain) XCUIRecorderNodeFinderMatch *ancestorNodeFinderMatch; // @synthesize ancestorNodeFinderMatch=_ancestorNodeFinderMatch; +@property(retain) NSMutableArray *unprocessedContainsMatches; // @synthesize unprocessedContainsMatches=_unprocessedContainsMatches; +@property(retain) NSMutableArray *mutableFoundNodeMatches; // @synthesize mutableFoundNodeMatches=_mutableFoundNodeMatches; +- (id)descendantsQueryNodeWithTargetElementTypeContainingElementsOfType:(unsigned long long)arg1 identifierValue:(id)arg2; +- (id)childrenQueryNodeWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)descendantsQueryNodeWithElementType:(unsigned long long)arg1; +- (id)descendantsQueryNodeWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)childAtIndexNodeWithTargetElementType; +- (id)childAtIndexNodeWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)uniqueChildNodeWithTargetElementType; +- (id)uniqueChildNodeWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)uniqueDescendantNodeWithTargetElementType; +- (id)uniqueDescendantNodeWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)descendantsWithTargetElementTypeContainingDescendantElementsWithType:(unsigned long long)arg1 identifierValue:(id)arg2; +- (id)childrenWithTargetElementType; +- (id)childrenWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)descendantsWithTargetElementType; +- (id)descendantsWithTargetElementTypeAndIdentifierValue:(id)arg1; +- (id)nextNodeFinderMatch; +- (id)_stringConstantForString:(id)arg1; +- (void)removeFromAncestorNodeFinderMatch; +- (void)invalidate; +- (id)initWithTargetSnapshot:(id)arg1 targetSnapshotIndex:(unsigned long long)arg2 ancestorMatch:(id)arg3 allowElementQueries:(BOOL)arg4 excludeUnlessNecessary:(BOOL)arg5 language:(unsigned long long)arg6 platform:(unsigned long long)arg7; + +@end diff --git a/PrivateHeaders/XCTest/XCUIRecorderNodeFinderMatch.h b/PrivateHeaders/XCTest/XCUIRecorderNodeFinderMatch.h new file mode 100644 index 0000000..b25e565 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIRecorderNodeFinderMatch.h @@ -0,0 +1,25 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableArray, NSSet, XCSourceCodeTreeNode, XCUIRecorderNodeFinder; + +@interface XCUIRecorderNodeFinderMatch : NSObject +{ + NSSet *_matchingSnapshots; + XCSourceCodeTreeNode *_node; + XCUIRecorderNodeFinder *_ancestorFinder; + NSMutableArray *_descendantFinders; +} +@property(retain) NSMutableArray *descendantFinders; // @synthesize descendantFinders=_descendantFinders; +@property(retain) XCUIRecorderNodeFinder *ancestorFinder; // @synthesize ancestorFinder=_ancestorFinder; +@property(retain) XCSourceCodeTreeNode *node; // @synthesize node=_node; +@property(copy) NSSet *matchingSnapshots; // @synthesize matchingSnapshots=_matchingSnapshots; + +- (void)invalidate; +- (id)nodeIncludingDescendants; +- (id)initWithNode:(id)arg1 matchingSnapshots:(id)arg2 ancestorFinder:(id)arg3; + +@end diff --git a/PrivateHeaders/XCTest/XCUIRecorderTimingMessage.h b/PrivateHeaders/XCTest/XCUIRecorderTimingMessage.h new file mode 100644 index 0000000..ba65f8a --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIRecorderTimingMessage.h @@ -0,0 +1,20 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString; + +@interface XCUIRecorderTimingMessage : NSObject +{ + double _start; + NSString *_message; +} +@property(copy) NSString *message; // @synthesize message=_message; +@property double start; // @synthesize start=_start; + ++ (id)descriptionForTimingMessages:(id)arg1; ++ (id)messageWithString:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCUIRecorderUtilities.h b/PrivateHeaders/XCTest/XCUIRecorderUtilities.h new file mode 100644 index 0000000..c908d7e --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIRecorderUtilities.h @@ -0,0 +1,45 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableString; + +@interface XCUIRecorderUtilities : NSObject +{ + unsigned long long _language; + unsigned long long _platform; + unsigned long long _compareSnapshotsLikePlatform; + id/*XCAccessibilityElement*/ _previousFocusedAccessibilityElement; + NSMutableString *_previousTyping; +} +@property(retain) NSMutableString *previousTyping; // @synthesize previousTyping=_previousTyping; +@property(retain) id/*XCAccessibilityElement*/ previousFocusedAccessibilityElement; // @synthesize previousFocusedAccessibilityElement=_previousFocusedAccessibilityElement; +@property unsigned long long _compareSnapshotsLikePlatform; // @synthesize _compareSnapshotsLikePlatform; +@property unsigned long long language; // @synthesize language=_language; +@property unsigned long long platform; // @synthesize platform=_platform; + ++ (id)applicationNodeForLanguage:(unsigned long long)arg1; ++ (unsigned long long)currentPlatform; + +- (id)performWithKeyModifiersAndBlockNodeForModifierFlags:(unsigned long long)arg1; +- (id)gestureNodesForKeyDownEventWithCharacters:(id)arg1 charactersIgnoringModifiers:(id)arg2 modifierFlags:(unsigned long long)arg3 focusedAccessibilityElement:(id)arg4 didAppendToPreviousString:(BOOL *)arg5; +- (id)_stringConstantForString:(id)arg1; +- (void)clearPreviousTyping; +- (id)nodeToFindElementForSnapshots:(id)arg1; +- (id)typeKeyNodeForKey:(id)arg1 modifierFlags:(unsigned long long)arg2; +- (id)typeStringNodeForString:(id)arg1; +- (id)stringForKeyModifierFlags:(unsigned long long)arg1; +- (id)simpleGestureNodeForMethodName:(id)arg1; +- (id)assertHasFocusNode; +- (id)remoteNodeWithButtonSymbolName:(id)arg1; +- (id)commentNodeWithString:(id)arg1; +- (id)applicationNode; +- (id)focusedAccessibilityElementForApplicationSnapshot:(id)arg1; +- (id)snapshotsForAccessibilityElement:(id)arg1 applicationSnapshot:(id)arg2; +- (id)snapshotInTreeStartingWithSnapshot:(id)arg1 forElement:(id)arg2; +- (id)_snapshotInTreeStartingWithSnapshot:(id)arg1 passingPredicateBlock:(CDUnknownBlockType)arg2; +- (id)nodeForOrientationChangeWithSymbolName:(id)arg1; + +@end diff --git a/PrivateHeaders/XCTest/XCUIScreen.h b/PrivateHeaders/XCTest/XCUIScreen.h new file mode 100644 index 0000000..3b23c24 --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIScreen.h @@ -0,0 +1,27 @@ +// +// Generated by class-dump 3.5 (64 bit) (Debug version compiled Nov 29 2017 14:55:25). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2015 by Steve Nygard. +// + +@interface XCUIScreen() +{ + _Bool _isMainScreen; + long long _displayID; +} +@property(readonly) _Bool isMainScreen; // @synthesize isMainScreen=_isMainScreen; +@property(readonly) long long displayID; // @synthesize displayID=_displayID; + +- (id)_clippedScreenshotData:(id)arg1 quality:(long long)arg2 rect:(struct CGRect)arg3 scale:(double)arg4; +- (id)_screenshotDataForQuality:(long long)arg1 rect:(struct CGRect)arg2 error:(id *)arg3; +- (id)screenshotDataForQuality:(long long)arg1 rect:(struct CGRect)arg2 error:(id *)arg3; +- (id)screenshotDataForQuality:(long long)arg1 rect:(struct CGRect)arg2; +- (id)_modernScreenshotDataForQuality:(long long)arg1 rect:(struct CGRect)arg2 error:(id *)arg3; +- (id)screenshot; +- (id)_imageFromData:(id)arg1; +- (double)scale; + +- (id)initWithDisplayID:(long long)arg1 isMainScreen:(_Bool)arg2 device:(id)arg3 screenDataSource:(id)arg4; + +@end + diff --git a/PrivateHeaders/XCTest/XCUIScreenDataSource-Protocol.h b/PrivateHeaders/XCTest/XCUIScreenDataSource-Protocol.h new file mode 100644 index 0000000..0c7d68c --- /dev/null +++ b/PrivateHeaders/XCTest/XCUIScreenDataSource-Protocol.h @@ -0,0 +1,19 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString; + +@protocol XCUIScreenDataSource +- (void)requestScreenshotOfScreenWithID:(long long)arg1 + withRect:(struct CGRect)arg2 + scale:(double)arg3 + formatUTI:(NSString *)arg4 + compressionQuality:(double)arg5 + withReply:(void (^)(NSData *, NSError *))arg6; +- (void)requestScaleForScreenWithIdentifier:(long long)arg1 completion:(void (^)(double, NSError *))arg2; +- (void)requestScreenIdentifiersWithCompletion:(void (^)(NSArray *, NSError *))arg1; +@end + diff --git a/PrivateHeaders/XCTest/_XCInternalTestRun.h b/PrivateHeaders/XCTest/_XCInternalTestRun.h new file mode 100644 index 0000000..b0bb0a4 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCInternalTestRun.h @@ -0,0 +1,43 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSDate, XCTest; + +@interface _XCInternalTestRun : NSObject +{ + XCTest *_test; + double _startTimeInterval; + double _stopTimeInterval; + unsigned long long _executionCount; + unsigned long long _failureCount; + unsigned long long _unexpectedExceptionCount; + BOOL _hasStarted; + BOOL _hasStopped; + unsigned long long _executionCountBeforeCrash; + unsigned long long _failureCountBeforeCrash; + unsigned long long _unexpectedExceptionCountBeforeCrash; +} + +@property unsigned long long unexpectedExceptionCountBeforeCrash; // @synthesize unexpectedExceptionCountBeforeCrash=_unexpectedExceptionCountBeforeCrash; +@property unsigned long long failureCountBeforeCrash; // @synthesize failureCountBeforeCrash=_failureCountBeforeCrash; +@property unsigned long long executionCountBeforeCrash; // @synthesize executionCountBeforeCrash=_executionCountBeforeCrash; +@property(readonly) BOOL hasStopped; // @synthesize hasStopped=_hasStopped; +@property(readonly) XCTest *test; // @synthesize test=_test; +@property(readonly) unsigned long long testCaseCount; +@property(readonly) unsigned long long unexpectedExceptionCount; +@property(readonly) unsigned long long failureCount; +@property(readonly) unsigned long long executionCount; +@property(readonly, copy) NSDate *stopDate; +@property(readonly, copy) NSDate *startDate; +@property(readonly) double testDuration; +@property(readonly) double totalDuration; + +- (id)initWithTest:(id)arg1; +- (void)stop; +- (void)start; +- (void)recordFailureWithDescription:(id)arg1 inFile:(id)arg2 atLine:(unsigned long long)arg3 expected:(BOOL)arg4; + +@end diff --git a/PrivateHeaders/XCTest/_XCKVOExpectationImplementation.h b/PrivateHeaders/XCTest/_XCKVOExpectationImplementation.h new file mode 100644 index 0000000..2c79aa5 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCKVOExpectationImplementation.h @@ -0,0 +1,32 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSObject, NSString, XCTKVOExpectation; + +@interface _XCKVOExpectationImplementation : NSObject +{ + XCTKVOExpectation *_expectation; + id _observedObject; + NSString *_keyPath; + id _expectedValue; + unsigned long long _options; + CDUnknownBlockType _handler; + NSObject *_queue; + BOOL _hasCleanedUp; +} +@property(readonly) unsigned long long options; // @synthesize options=_options; +@property(readonly) id expectedValue; // @synthesize expectedValue=_expectedValue; +@property(readonly, copy) NSString *keyPath; // @synthesize keyPath=_keyPath; +@property(readonly) id observedObject; // @synthesize observedObject=_observedObject; +@property(copy) CDUnknownBlockType handler; + +- (void)cleanup; +- (void)observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:(id)arg3 context:(void *)arg4; +- (id)initWithKeyPath:(id)arg1 object:(id)arg2 expectedValue:(id)arg3 expectation:(id)arg4 options:(unsigned long long)arg5; + +@end diff --git a/PrivateHeaders/XCTest/_XCTDarwinNotificationExpectationImplementation.h b/PrivateHeaders/XCTest/_XCTDarwinNotificationExpectationImplementation.h new file mode 100644 index 0000000..f3d314b --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTDarwinNotificationExpectationImplementation.h @@ -0,0 +1,27 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSObject, NSString, XCTDarwinNotificationExpectation; + +@interface _XCTDarwinNotificationExpectationImplementation : NSObject +{ + XCTDarwinNotificationExpectation *_expectation; + NSString *_notificationName; + int _notifyToken; + CDUnknownBlockType _handler; + NSObject *_queue; + BOOL _hasCleanedUp; +} +@property(readonly, copy) NSString *notificationName; // @synthesize notificationName=_notificationName; +@property(copy) CDUnknownBlockType handler; + +- (void)cleanup; +- (void)_handleNotification; +- (id)initWithNotificationName:(id)arg1 expectation:(id)arg2; + +@end diff --git a/PrivateHeaders/XCTest/_XCTNSNotificationExpectationImplementation.h b/PrivateHeaders/XCTest/_XCTNSNotificationExpectationImplementation.h new file mode 100644 index 0000000..997ccd1 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTNSNotificationExpectationImplementation.h @@ -0,0 +1,30 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSNotificationCenter, NSObject, NSString, XCTNSNotificationExpectation; + +@interface _XCTNSNotificationExpectationImplementation : NSObject +{ + XCTNSNotificationExpectation *_expectation; + id _observedObject; + NSString *_notificationName; + NSNotificationCenter *_notificationCenter; + CDUnknownBlockType _handler; + NSObject *_queue; + BOOL _hasCleanedUp; +} +@property(readonly) NSNotificationCenter *notificationCenter; // @synthesize notificationCenter=_notificationCenter; +@property(readonly, copy) NSString *notificationName; // @synthesize notificationName=_notificationName; +@property(readonly) id observedObject; // @synthesize observedObject=_observedObject; +@property(copy) CDUnknownBlockType handler; + +- (void)cleanup; +- (void)_observeExpectedNotification:(id)arg1; +- (id)initWithName:(id)arg1 object:(id)arg2 notificationCenter:(id)arg3 expectation:(id)arg4; + +@end diff --git a/PrivateHeaders/XCTest/_XCTNSPredicateExpectationImplementation.h b/PrivateHeaders/XCTest/_XCTNSPredicateExpectationImplementation.h new file mode 100644 index 0000000..5c1e938 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTNSPredicateExpectationImplementation.h @@ -0,0 +1,29 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSObject, NSPredicate, NSString, NSTimer, XCTNSPredicateExpectation; + +@interface _XCTNSPredicateExpectationImplementation : NSObject +{ + XCTNSPredicateExpectation *_expectation; + id _object; + NSPredicate *_predicate; + CDUnknownBlockType _handler; + NSTimer *_timer; + NSObject *_queue; + BOOL _hasCleanedUp; +} +@property(readonly, copy) NSPredicate *predicate; // @synthesize predicate=_predicate; +@property(readonly) id object; // @synthesize object=_object; +@property(copy) CDUnknownBlockType handler; + +- (void)cleanup; +- (void)_considerFulfilling; +- (id)initWithPredicate:(id)arg1 object:(id)arg2 expectation:(id)arg3; + +@end diff --git a/PrivateHeaders/XCTest/_XCTWaiterImpl.h b/PrivateHeaders/XCTest/_XCTWaiterImpl.h new file mode 100644 index 0000000..596c209 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTWaiterImpl.h @@ -0,0 +1,41 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import "NSObject.h" + +@class NSArray, NSMutableArray, NSObject, XCTWaiterManager; + +@interface _XCTWaiterImpl : NSObject +{ + id _delegate; + XCTWaiterManager *_manager; + NSArray *_waitCallStackReturnAddresses; + NSObject *_queue; + NSObject *_delegateQueue; + NSArray *_expectations; + NSMutableArray *_fulfilledExpectations; + struct __CFRunLoop *_waitingRunLoop; + long long _state; + double _timeout; + long long _result; + BOOL _enforceOrderOfFulfillment; +} +@property BOOL enforceOrderOfFulfillment; // @synthesize enforceOrderOfFulfillment=_enforceOrderOfFulfillment; +@property long long result; // @synthesize result=_result; +@property long long state; // @synthesize state=_state; +@property(readonly, nonatomic) NSMutableArray *fulfilledExpectations; // @synthesize fulfilledExpectations=_fulfilledExpectations; +@property(copy, nonatomic) NSArray *expectations; // @synthesize expectations=_expectations; +@property(readonly, nonatomic) NSObject *delegateQueue; // @synthesize delegateQueue=_delegateQueue; +@property(readonly, nonatomic) NSObject *queue; // @synthesize queue=_queue; +@property XCTWaiterManager *manager; // @synthesize manager=_manager; +@property id delegate; // @synthesize delegate=_delegate; +@property double timeout; // @synthesize timeout=_timeout; +@property struct __CFRunLoop *waitingRunLoop; // @synthesize waitingRunLoop=_waitingRunLoop; +@property(copy) NSArray *waitCallStackReturnAddresses; // @synthesize waitCallStackReturnAddresses=_waitCallStackReturnAddresses; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/_XCTestCaseImplementation.h b/PrivateHeaders/XCTest/_XCTestCaseImplementation.h new file mode 100644 index 0000000..e72d205 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestCaseImplementation.h @@ -0,0 +1,70 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSArray, NSInvocation, NSMutableArray, NSMutableDictionary, NSMutableSet, NSString, XCTestCaseRun, XCTestContext, XCTestExpectationWaiter, XCTWaiter; + +#import + +@interface _XCTestCaseImplementation : NSObject +{ + NSInvocation *_invocation; + XCTestCaseRun *_testCaseRun; + BOOL _continueAfterFailure; + NSMutableSet *_expectations; + NSArray *_activePerformanceMetricIDs; + NSMutableDictionary *_perfMetricsForID; + unsigned long long _startWallClockTime; + struct time_value _startUserTime; + struct time_value _startSystemTime; + unsigned long long _measuringIteration; + BOOL _isMeasuringMetrics; + BOOL _didMeasureMetrics; + BOOL _didStartMeasuring; + BOOL _didStopMeasuring; + NSString *_filePathForUnexpectedFailure; + unsigned long long _lineNumberForUnexpectedFailure; + unsigned long long _callAddressForCurrentWait; + NSArray *_callAddressesForLastCreatedExpectation; + long long _runLoopNestingCount; + XCTWaiter *_currentWaiter; + NSMutableArray *_failureRecords; + BOOL _shouldHaltWhenReceivesControl; + BOOL _shouldIgnoreSubsequentFailures; + NSMutableArray *_activityRecordStack; + XCTestContext *_testContext; +} + +@property(readonly) XCTestContext *testContext; // @synthesize testContext=_testContext; +@property(retain, nonatomic) XCTWaiter *currentWaiter; // @synthesize currentWaiter=_currentWaiter; +@property(retain, nonatomic) NSMutableArray *activityRecordStack; // @synthesize activityRecordStack=_activityRecordStack; +@property BOOL shouldIgnoreSubsequentFailures; // @synthesize shouldIgnoreSubsequentFailures=_shouldIgnoreSubsequentFailures; +@property BOOL shouldHaltWhenReceivesControl; // @synthesize shouldHaltWhenReceivesControl=_shouldHaltWhenReceivesControl; +@property(retain, nonatomic) NSMutableArray *failureRecords; // @synthesize failureRecords=_failureRecords; +@property long long runLoopNestingCount; // @synthesize runLoopNestingCount=_runLoopNestingCount; +@property(copy) NSArray *callAddressesForLastCreatedExpectation; // @synthesize callAddressesForLastCreatedExpectation=_callAddressesForLastCreatedExpectation; +@property unsigned long long callAddressForCurrentWait; // @synthesize callAddressForCurrentWait=_callAddressForCurrentWait; +@property unsigned long long lineNumberForUnexpectedFailure; // @synthesize lineNumberForUnexpectedFailure=_lineNumberForUnexpectedFailure; +@property(copy) NSString *filePathForUnexpectedFailure; // @synthesize filePathForUnexpectedFailure=_filePathForUnexpectedFailure; +@property(retain, nonatomic) NSMutableSet *expectations; // @synthesize expectations=_expectations; +@property BOOL didStopMeasuring; // @synthesize didStopMeasuring=_didStopMeasuring; +@property BOOL didStartMeasuring; // @synthesize didStartMeasuring=_didStartMeasuring; +@property BOOL didMeasureMetrics; // @synthesize didMeasureMetrics=_didMeasureMetrics; +@property BOOL isMeasuringMetrics; // @synthesize isMeasuringMetrics=_isMeasuringMetrics; +@property unsigned long long measuringIteration; // @synthesize measuringIteration=_measuringIteration; +@property struct time_value startUserTime; // @synthesize startUserTime=_startUserTime; +@property struct time_value startSystemTime; // @synthesize startSystemTime=_startSystemTime; +@property unsigned long long startWallClockTime; // @synthesize startWallClockTime=_startWallClockTime; +@property(retain) NSMutableDictionary *perfMetricsForID; // @synthesize perfMetricsForID=_perfMetricsForID; +@property(copy) NSArray *activePerformanceMetricIDs; // @synthesize activePerformanceMetricIDs=_activePerformanceMetricIDs; +@property BOOL continueAfterFailure; // @synthesize continueAfterFailure=_continueAfterFailure; +@property(retain) XCTestCaseRun *testCaseRun; // @synthesize testCaseRun=_testCaseRun; +@property(retain) NSInvocation *invocation; // @synthesize invocation=_invocation; + +- (void)resetExpectations; +- (void)addExpectation:(id)arg1; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/_XCTestCaseInterruptionException.h b/PrivateHeaders/XCTest/_XCTestCaseInterruptionException.h new file mode 100644 index 0000000..50c2557 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestCaseInterruptionException.h @@ -0,0 +1,13 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@interface _XCTestCaseInterruptionException : NSException +{ +} + ++ (void)interruptTest; + +@end diff --git a/PrivateHeaders/XCTest/_XCTestExpectationImplementation.h b/PrivateHeaders/XCTest/_XCTestExpectationImplementation.h new file mode 100644 index 0000000..43b9f85 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestExpectationImplementation.h @@ -0,0 +1,40 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSString, XCTestCase; + +@interface _XCTestExpectationImplementation : NSObject +{ + BOOL _fulfilled; + NSString *_expectationDescription; + id _delegate; + BOOL _hasBeenWaitedOn; + unsigned long long _expectedFulfillmentCount; + unsigned long long _numberOfFulfillments; + unsigned long long _fulfillmentToken; + NSArray *_fulfillCallStackReturnAddresses; + BOOL _inverted; + BOOL _assertForOverFulfill; + NSObject *_queue; + NSObject *_delegateQueue; +} + +@property(readonly, nonatomic) NSObject *delegateQueue; // @synthesize delegateQueue=_delegateQueue; +@property(readonly, nonatomic) NSObject *queue; // @synthesize queue=_queue; +@property(nonatomic) unsigned long long numberOfFulfillments; // @synthesize numberOfFulfillments=_numberOfFulfillments; +@property(nonatomic) unsigned long long expectedFulfillmentCount; // @synthesize expectedFulfillmentCount=_expectedFulfillmentCount; +@property(copy) NSArray *fulfillCallStackReturnAddresses; // @synthesize fulfillCallStackReturnAddresses=_fulfillCallStackReturnAddresses; +@property unsigned long long fulfillmentToken; // @synthesize fulfillmentToken=_fulfillmentToken; +@property BOOL assertForOverFulfill; // @synthesize assertForOverFulfill=_assertForOverFulfill; +@property BOOL inverted; // @synthesize inverted=_inverted; +@property BOOL hasBeenWaitedOn; // @synthesize hasBeenWaitedOn=_hasBeenWaitedOn; +@property(retain) id delegate; // @synthesize delegate=_delegate; +@property(copy) NSString *expectationDescription; // @synthesize expectationDescription=_expectationDescription; +@property BOOL fulfilled; // @synthesize fulfilled=_fulfilled; + +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/_XCTestImplementation.h b/PrivateHeaders/XCTest/_XCTestImplementation.h new file mode 100644 index 0000000..72d6351 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestImplementation.h @@ -0,0 +1,14 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class XCTestRun; + +@interface _XCTestImplementation : NSObject +{ + XCTestRun *_testRun; +} +@property(retain) XCTestRun *testRun; // @synthesize testRun=_testRun; +@end diff --git a/PrivateHeaders/XCTest/_XCTestObservationCenterImplementation.h b/PrivateHeaders/XCTest/_XCTestObservationCenterImplementation.h new file mode 100644 index 0000000..481934e --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestObservationCenterImplementation.h @@ -0,0 +1,19 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +@class NSMutableSet; + +@interface _XCTestObservationCenterImplementation : NSObject +{ + NSMutableArray *_observers; + BOOL _suspended; +} + +@property BOOL suspended; // @synthesize suspended=_suspended; +@property(retain) NSMutableArray *observers; // @synthesize observers=_observers; +- (id)init; + +@end diff --git a/PrivateHeaders/XCTest/_XCTestSuiteImplementation.h b/PrivateHeaders/XCTest/_XCTestSuiteImplementation.h new file mode 100644 index 0000000..93c6345 --- /dev/null +++ b/PrivateHeaders/XCTest/_XCTestSuiteImplementation.h @@ -0,0 +1,23 @@ +// +// Generated by class-dump 3.5 (64 bit). +// +// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. +// + +#import + +@class NSMutableArray, NSString, XCTestConfiguration; + +@interface _XCTestSuiteImplementation : XCTest +{ + NSString *_name; + NSMutableArray *_tests; + XCTestConfiguration *_testConfiguration; +} +@property(retain) XCTestConfiguration *testConfiguration; // @synthesize testConfiguration=_testConfiguration; +@property(retain) NSMutableArray *tests; // @synthesize tests=_tests; +@property(copy) NSString *name; // @synthesize name=_name; + +- (id)initWithName:(id)arg1; + +@end diff --git a/README.md b/README.md new file mode 100644 index 0000000..a367763 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# WebDriverAgent + +[![NPM version](http://img.shields.io/npm/v/appium-webdriveragent.svg)](https://npmjs.org/package/appium-webdriveragent) +[![Downloads](http://img.shields.io/npm/dm/appium-webdriveragent.svg)](https://npmjs.org/package/appium-webdriveragent) + +[![Release](https://github.com/appium/WebDriverAgent/actions/workflows/publish.js.yml/badge.svg)](https://github.com/appium/WebDriverAgent/actions/workflows/publish.js.yml) + +[![GitHub license](https://img.shields.io/badge/license-BSD-lightgrey.svg)](LICENSE) + +WebDriverAgent is a [WebDriver server](https://w3c.github.io/webdriver/webdriver-spec.html) implementation for iOS that can be used to remote control iOS devices. It allows you to launch & kill applications, tap & scroll views or confirm view presence on a screen. This makes it a perfect tool for application end-to-end testing or general purpose device automation. It works by linking `XCTest.framework` and calling Apple's API to execute commands directly on a device. WebDriverAgent is developed for end-to-end testing and is successfully adopted by [Appium](http://appium.io) via [XCUITest driver](https://github.com/appium/appium-xcuitest-driver). + +## Features + * Both iOS and tvOS platforms are supported with devices & simulators + * Implements most of [WebDriver Spec](https://w3c.github.io/webdriver/webdriver-spec.html) + * Implements part of [Mobile JSON Wire Protocol Spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md) + * USB support for devices is implemented via [appium-ios-device](https://github.com/appium/appium-ios-device) library and has zero dependencies on third-party tools. + * Easy development cycle as it can be launched & debugged directly via Xcode + * Use [Mac2Driver](https://github.com/appium/appium-mac2-driver) to automate macOS apps + +## Getting Started On This Repository + +You need to have Node.js installed for this project. + +After it is finished you can simply open `WebDriverAgent.xcodeproj` and start `WebDriverAgentRunner` test +and start sending [requests](https://github.com/facebook/WebDriverAgent/wiki/Queries). + +More about how to start WebDriverAgent [here](https://github.com/facebook/WebDriverAgent/wiki/Starting-WebDriverAgent). + +## Known Issues +If you are having some issues please checkout [wiki](https://github.com/facebook/WebDriverAgent/wiki/Common-Issues) first. + +## For Contributors +If you want to help us out, you are more than welcome to. However please make sure you have followed the guidelines in [CONTRIBUTING](CONTRIBUTING.md). + +## Creating Bundles + +`npm run bundle` + +Then, you find `WebDriverAgentRunner-Runner-sim-.zip` for iOS and `WebDriverAgentRunner-Runner-tv_sim-.zip` for tvOS files in the current directory. + +## License + +[`WebDriverAgent` is BSD-licensed](LICENSE). + +## Third Party Sources + +WebDriverAgent depends on the following third-party frameworks: +- [CocoaHTTPServer](https://github.com/robbiehanson/CocoaHTTPServer) +- [RoutingHTTPServer](https://github.com/mattstevens/RoutingHTTPServer) + +These projects haven't been maintained in a while. That's why the source code of these +projects has been integrated directly in the WebDriverAgent source tree. + +You can find the source files and their licenses in the `WebDriverAgentLib/Vendor` directory. + +Have fun! diff --git a/Scripts/build-webdriveragent.js b/Scripts/build-webdriveragent.js new file mode 100644 index 0000000..b37bcbb --- /dev/null +++ b/Scripts/build-webdriveragent.js @@ -0,0 +1,79 @@ +const path = require('path'); +const { asyncify } = require('asyncbox'); +const { logger, fs } = require('@appium/support'); +const { exec } = require('teen_process'); +const xcode = require('appium-xcode'); + +const LOG = new logger.getLogger('WDABuild'); +const ROOT_DIR = path.resolve(__dirname, '..'); +const DERIVED_DATA_PATH = `${ROOT_DIR}/wdaBuild`; +const WDA_BUNDLE = 'WebDriverAgentRunner-Runner.app'; +const WDA_BUNDLE_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-iphonesimulator'); + +const WDA_BUNDLE_TV = 'WebDriverAgentRunner_tvOS-Runner.app'; +const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-appletvsimulator'); + +const TARGETS = ['runner', 'tv_runner']; +const SDKS = ['sim', 'tv_sim']; + +async function buildWebDriverAgent (xcodeVersion) { + const target = process.env.TARGET; + const sdk = process.env.SDK; + + if (!TARGETS.includes(target)) { + throw Error(`Please set TARGETS environment variable from the supported targets ${JSON.stringify(TARGETS)}`); + } + + if (!SDKS.includes(sdk)) { + throw Error(`Please set SDK environment variable from the supported SDKs ${JSON.stringify(SDKS)}`); + } + + + LOG.info(`Cleaning ${DERIVED_DATA_PATH} if exists`); + try { + await exec('xcodebuild', ['clean', '-derivedDataPath', DERIVED_DATA_PATH, '-scheme', 'WebDriverAgentRunner'], { + cwd: ROOT_DIR + }); + } catch {} + + // Get Xcode version + xcodeVersion = xcodeVersion || await xcode.getVersion(); + LOG.info(`Building WebDriverAgent for iOS using Xcode version '${xcodeVersion}'`); + + // Clean and build + try { + await exec('/bin/bash', ['./Scripts/build.sh'], { + env: {TARGET: target, SDK: sdk, DERIVED_DATA_PATH}, + cwd: ROOT_DIR + }); + } catch (e) { + LOG.error(`===FAILED TO BUILD FOR ${xcodeVersion}`); + LOG.error(e.stderr); + throw e; + } + + const isTv = target === 'tv_runner'; + const bundle = isTv ? WDA_BUNDLE_TV : WDA_BUNDLE; + const bundle_path = isTv ? WDA_BUNDLE_TV_PATH : WDA_BUNDLE_PATH; + + const zipName = `WebDriverAgentRunner-Runner-${sdk}-${xcodeVersion}.zip`; + LOG.info(`Creating ${zipName} which includes ${bundle}`); + const appBundleZipPath = path.join(ROOT_DIR, zipName); + await fs.rimraf(appBundleZipPath); + LOG.info(`Created './${zipName}'`); + try { + await exec('xattr', ['-cr', bundle], {cwd: bundle_path}); + await exec('zip', ['-qr', appBundleZipPath, bundle], {cwd: bundle_path}); + } catch (e) { + LOG.error(`===FAILED TO ZIP ARCHIVE`); + LOG.error(e.stderr); + throw e; + } + LOG.info(`Zip bundled at "${appBundleZipPath}"`); +} + +if (require.main === module) { + asyncify(buildWebDriverAgent); +} + +module.exports = buildWebDriverAgent; diff --git a/Scripts/build.sh b/Scripts/build.sh new file mode 100755 index 0000000..698d58b --- /dev/null +++ b/Scripts/build.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Copyright (c) 2015-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# + +set -ex + +function define_xc_macros() { + XC_MACROS="CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO" + + case "$TARGET" in + "lib" ) XC_TARGET="WebDriverAgentLib";; + "runner" ) XC_TARGET="WebDriverAgentRunner";; + "tv_lib" ) XC_TARGET="WebDriverAgentLib_tvOS";; + "tv_runner" ) XC_TARGET="WebDriverAgentRunner_tvOS";; + *) echo "Unknown TARGET"; exit 1 ;; + esac + + case "${DEST:-}" in + "iphone" ) XC_DESTINATION="name=`echo $IPHONE_MODEL | tr -d "'"`,OS=$IOS_VERSION";; + "ipad" ) XC_DESTINATION="name=`echo $IPAD_MODEL | tr -d "'"`,OS=$IOS_VERSION";; + "tv" ) XC_DESTINATION="name=`echo $TV_MODEL | tr -d "'"`,OS=$TV_VERSION";; + "generic" ) XC_DESTINATION="generic/platform=iOS";; + "tv_generic" ) XC_DESTINATION="generic/platform=tvOS" XC_MACROS="${XC_MACROS} ARCHS=arm64";; # tvOS only supports arm64 + esac + + case "$ACTION" in + "build" ) XC_ACTION="build";; + "analyze" ) + XC_ACTION="analyze" + XC_MACROS="${XC_MACROS} CLANG_ANALYZER_OUTPUT=plist-html CLANG_ANALYZER_OUTPUT_DIR=\"$(pwd)/clang\"" + ;; + "unit_test" ) XC_ACTION="test -only-testing:UnitTests";; + "tv_unit_test" ) XC_ACTION="test -only-testing:UnitTests_tvOS";; + esac + + case "$SDK" in + "sim" ) XC_SDK="iphonesimulator";; + "device" ) XC_SDK="iphoneos";; + "tv_sim" ) XC_SDK="appletvsimulator";; + "tv_device" ) XC_SDK="appletvos";; + *) echo "Unknown SDK"; exit 1 ;; + esac + + case "${CODE_SIGN:-}" in + "no" ) XC_MACROS="${XC_MACROS} CODE_SIGNING_ALLOWED=NO";; + esac +} + +function analyze() { + xcbuild + if [[ -z $(find clang -name "*.html") ]]; then + echo "Static Analyzer found no issues" + else + echo "Static Analyzer found some issues" + exit 1 + fi +} + +function xcbuild() { + destination="" + output_command=cat + if [ $(which xcpretty) ] ; then + output_command=xcpretty + fi + + XC_BUILD_ARGS=(-project "WebDriverAgent.xcodeproj") + XC_BUILD_ARGS+=(-scheme "$XC_TARGET") + XC_BUILD_ARGS+=(-sdk "$XC_SDK") + XC_BUILD_ARGS+=($XC_ACTION) + if [[ -n "$XC_DESTINATION" ]]; then + XC_BUILD_ARGS+=(-destination "${XC_DESTINATION}") + fi + if [[ -n "$DERIVED_DATA_PATH" ]]; then + XC_BUILD_ARGS+=(-derivedDataPath ${DERIVED_DATA_PATH}) + fi + XC_BUILD_ARGS+=($XC_MACROS $EXTRA_XC_ARGS) + + xcodebuild "${XC_BUILD_ARGS[@]}" | $output_command && exit ${PIPESTATUS[0]} + +} + +function fastlane_test() { + bundle install + + if [[ -n "$XC_DESTINATION" ]]; then + SDK="$XC_SDK" DEST="$XC_DESTINATION" SCHEME="$1" bundle exec fastlane test + else + SDK="$XC_SDK" SCHEME="$1" bundle exec fastlane test + fi +} + +define_xc_macros +case "$ACTION" in + "analyze" ) analyze ;; + "int_test_1" ) fastlane_test IntegrationTests_1 ;; + "int_test_2" ) fastlane_test IntegrationTests_2 ;; + "int_test_3" ) fastlane_test IntegrationTests_3 ;; + *) xcbuild ;; +esac diff --git a/Scripts/ci/build-real.sh b/Scripts/ci/build-real.sh new file mode 100755 index 0000000..bb41b0b --- /dev/null +++ b/Scripts/ci/build-real.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath $DERIVED_DATA_PATH \ + -scheme $SCHEME \ + -destination "$DESTINATION" \ + CODE_SIGNING_ALLOWED=NO ARCHS=arm64 + +pushd $WD + +# The reason why here excludes several frameworks are: +# - to remove test packages to refer to the device local instead of embedded ones +# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, +# XCUIAutomation.framework, XCUnit.framework. +# This can be excluded only for real devices. +# - Xcode 16 started generating 5.9MB of 'Testing.framework', but it might not be necessary for WDA. +# - libXCTestSwiftSupport is used for Swift testing. WDA doesn't include Swift stuff, thus this is not needed. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app \ + -x "$SCHEME-Runner.app/Frameworks/XC*.framework*" \ + "$SCHEME-Runner.app/Frameworks/Testing.framework*" \ + "$SCHEME-Runner.app/Frameworks/libXCTestSwiftSupport.dylib" +popd +mv $WD/$ZIP_PKG_NAME ./ diff --git a/Scripts/ci/build-sim.sh b/Scripts/ci/build-sim.sh new file mode 100755 index 0000000..b04cb48 --- /dev/null +++ b/Scripts/ci/build-sim.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath $DERIVED_DATA_PATH \ + -scheme $SCHEME \ + -destination "$DESTINATION" \ + CODE_SIGNING_ALLOWED=NO ARCHS=$ARCHS + +pushd $WD + +# Simulators might have an issue to lauch if we drop frameworks even we don't use them. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app +popd +mv $WD/$ZIP_PKG_NAME ./ diff --git a/Scripts/fetch-prebuilt-wda.js b/Scripts/fetch-prebuilt-wda.js new file mode 100644 index 0000000..4bd6840 --- /dev/null +++ b/Scripts/fetch-prebuilt-wda.js @@ -0,0 +1,61 @@ +const path = require('path'); +const axios = require('axios'); +const { asyncify } = require('asyncbox'); +const { logger, fs, mkdirp, net } = require('@appium/support'); +const _ = require('lodash'); +const B = require('bluebird'); + +const log = logger.getLogger('WDA'); + +async function fetchPrebuiltWebDriverAgentAssets () { + const tag = require('../package.json').version; + log.info(`Getting links to webdriveragent release ${tag}`); + const downloadUrl = `https://api.github.com/repos/appium/webdriveragent/releases/tags/v${tag}`; + log.info(`Getting WDA release ${downloadUrl}`); + let releases; + try { + releases = (await axios({ + url: downloadUrl, + headers: { + 'user-agent': 'appium', + 'accept': 'application/json, */*', + }, + })).data; + } catch (e) { + throw new Error(`Could not fetch endpoint ${downloadUrl}. Reason: ${e.message}`); + } + + const webdriveragentsDir = path.resolve(__dirname, '..', 'prebuilt-agents'); + log.info(`Creating webdriveragents directory at: ${webdriveragentsDir}`); + await fs.rimraf(webdriveragentsDir); + await mkdirp(webdriveragentsDir); + + // Define a method that does a streaming download of an asset + async function downloadAgent (url, targetPath) { + try { + await net.downloadFile(url, targetPath); + } catch (err) { + throw new Error(`Problem downloading webdriveragent from url ${url}: ${err.message}`); + } + } + + log.info(`Downloading assets to: ${webdriveragentsDir}`); + const agentsDownloading = []; + for (const asset of releases.assets) { + const url = asset.browser_download_url; + log.info(`Downloading: ${url}`); + try { + const nameOfAgent = _.last(url.split('/')); + agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent))); + } catch { } + } + + // Wait for them all to finish + return await B.all(agentsDownloading); +} + +if (require.main === module) { + asyncify(fetchPrebuiltWebDriverAgentAssets); +} + +module.exports = fetchPrebuiltWebDriverAgentAssets; diff --git a/Scripts/update-wda-version.js b/Scripts/update-wda-version.js new file mode 100644 index 0000000..142efdc --- /dev/null +++ b/Scripts/update-wda-version.js @@ -0,0 +1,41 @@ +const {plist, logger} = require('@appium/support'); +const path = require('node:path'); +const semver = require('semver'); + +const log = logger.getLogger('Versioner'); + +/** + * @param {string} argName + * @returns {string|null} + */ +function parseArgValue (argName) { + const argNamePattern = new RegExp(`^--${argName}\\b`); + for (let i = 1; i < process.argv.length; ++i) { + const arg = process.argv[i]; + if (argNamePattern.test(arg)) { + return arg.includes('=') ? arg.split('=')[1] : process.argv[i + 1]; + } + } + return null; +} + +async function updateWdaVersion() { + const newVersion = parseArgValue('package-version'); + if (!newVersion) { + throw new Error('No package version argument (use `--package-version=xxx`)'); + } + if (!semver.valid(newVersion)) { + throw new Error( + `Invalid version specified '${newVersion}'. Version should be in the form '1.2.3'` + ); + } + + const libManifest = path.resolve('WebDriverAgentLib', 'Info.plist'); + log.info(`Updating the WebDriverAgent manifest at '${libManifest}' to version '${newVersion}'`); + await plist.updatePlistFile(libManifest, { + CFBundleShortVersionString: newVersion, + CFBundleVersion: newVersion, + }, false); +} + +(async () => await updateWdaVersion())(); diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj new file mode 100644 index 0000000..67ace5c --- /dev/null +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -0,0 +1,4551 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; + 1357E296233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; + 1357E297233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; + 13815F6F2328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */; }; + 13815F702328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */; }; + 13815F712328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */; }; + 13815F722328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */; }; + 13DE7A43287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A41287C2A8D003243C6 /* FBXCAccessibilityElement.h */; }; + 13DE7A44287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A41287C2A8D003243C6 /* FBXCAccessibilityElement.h */; }; + 13DE7A45287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A42287C2A8D003243C6 /* FBXCAccessibilityElement.m */; }; + 13DE7A46287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A42287C2A8D003243C6 /* FBXCAccessibilityElement.m */; }; + 13DE7A49287C4005003243C6 /* FBXCDeviceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */; }; + 13DE7A4A287C4005003243C6 /* FBXCDeviceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */; }; + 13DE7A4B287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; + 13DE7A4C287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A51287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; + 13DE7A52287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; + 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; + 13DE7A58287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; + 13DE7A5B287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */; }; + 13DE7A5C287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */; }; + 13DE7A5D287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A5A287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m */; }; + 13DE7A5E287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A5A287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m */; }; + 13FFF2F2287DBEE600E561E4 /* XCElementSnapshotDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = 13FFF2F1287DBEE600E561E4 /* XCElementSnapshotDouble.m */; }; + 315A15012518CB8700A3A064 /* TouchableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 315A15002518CB8700A3A064 /* TouchableView.m */; }; + 315A15072518CC2800A3A064 /* TouchSpotView.m in Sources */ = {isa = PBXBuildFile; fileRef = 315A15062518CC2800A3A064 /* TouchSpotView.m */; }; + 315A150A2518D6F400A3A064 /* TouchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 315A15092518D6F400A3A064 /* TouchViewController.m */; }; + 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; + 63CCF91221ECE4C700E94ABD /* FBImageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */; }; + 63CCF91321ECE4C700E94ABD /* FBImageProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */; }; + 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; + 63FD950321F9D06100A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; + 63FD950421F9D06200A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; + 641EE3452240C1C800173FCB /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; }; + 641EE5D72240C5CA00173FCB /* FBScreenshotCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75F1CAEDF0C008C271F /* FBScreenshotCommands.m */; }; + 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; + 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; + 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; + 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */; }; + 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; + 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */; }; + 641EE5E02240C5CA00173FCB /* FBTCPSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 715557D2211DBCE700613B26 /* FBTCPSocket.m */; }; + 641EE5E12240C5CA00173FCB /* FBErrorBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */; }; + 641EE5E22240C5CA00173FCB /* XCUIElement+FBClassChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */; }; + 641EE5E32240C5CA00173FCB /* NSExpression+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71555A3C1DEC460A007D4A8B /* NSExpression+FBFormat.m */; }; + 641EE5E42240C5CA00173FCB /* XCUIApplication+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */; }; + 641EE5E52240C5CA00173FCB /* FBKeyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3A18651CDE734B00DE4205 /* FBKeyboard.m */; }; + 641EE5E62240C5CA00173FCB /* FBElementUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */; }; + 641EE5E72240C5CA00173FCB /* FBW3CActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */; }; + 641EE5E92240C5CA00173FCB /* FBFailureProofTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */; }; + 641EE5EA2240C5CA00173FCB /* XCUIElement+FBIsVisible.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */; }; + 641EE5EB2240C5CA00173FCB /* XCUIElement+FBFind.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */; }; + 641EE5EC2240C5CA00173FCB /* FBResponsePayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7831CAEDF0C008C271F /* FBResponsePayload.m */; }; + 641EE5ED2240C5CA00173FCB /* FBRoute.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7851CAEDF0C008C271F /* FBRoute.m */; }; + 641EE5EE2240C5CA00173FCB /* NSString+FBVisualLength.m in Sources */ = {isa = PBXBuildFile; fileRef = EE0D1F601EBCDCF7006A3123 /* NSString+FBVisualLength.m */; }; + 641EE5EF2240C5CA00173FCB /* FBRunLoopSpinner.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */; }; + 641EE5F02240C5CA00173FCB /* FBAlertsMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */; }; + 641EE5F12240C5CA00173FCB /* FBClassChainQueryParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */; }; + 641EE5F22240C5CA00173FCB /* NSPredicate+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */; }; + 641EE5F42240C5CA00173FCB /* XCUIDevice+FBRotation.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE3763E1D59F81400ED88DD /* XCUIDevice+FBRotation.m */; }; + 641EE5F52240C5CA00173FCB /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; + 641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; }; + 641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */; }; + 641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; + 641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */; }; + 641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */; }; + 641EE5FE2240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; }; + 641EE5FF2240C5CA00173FCB /* XCUIElement+FBForceTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */; }; + 641EE6002240C5CA00173FCB /* FBTouchActionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */; }; + 641EE6012240C5CA00173FCB /* FBImageProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */; }; + 641EE6022240C5CA00173FCB /* FBTouchIDCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */; }; + 641EE6032240C5CA00173FCB /* FBDebugCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */; }; + 641EE6042240C5CA00173FCB /* NSString+FBXMLSafeString.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */; }; + 641EE6052240C5CA00173FCB /* FBUnknownCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */; }; + 641EE6062240C5CA00173FCB /* FBOrientationCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */; }; + 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */; }; + 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */; }; + 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A41CF7A43900275851 /* FBLogger.m */; }; + 641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */; }; + 641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; }; + 641EE60D2240C5CA00173FCB /* XCTestPrivateSymbols.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */; }; + 641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */ = {isa = PBXBuildFile; fileRef = AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */; }; + 641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */; }; + 641EE6102240C5CA00173FCB /* FBImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 7150348621A6DAD600A0F4BA /* FBImageUtils.m */; }; + 641EE6112240C5CA00173FCB /* FBSession.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78B1CAEDF0C008C271F /* FBSession.m */; }; + 641EE6122240C5CA00173FCB /* FBFindElementCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7591CAEDF0C008C271F /* FBFindElementCommands.m */; }; + 641EE6132240C5CA00173FCB /* FBDebugLogDelegateDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */; }; + 641EE6142240C5CA00173FCB /* FBAlertViewCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */; }; + 641EE6152240C5CA00173FCB /* XCUIElement+FBScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */; }; + 641EE6162240C5CA00173FCB /* FBSessionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7611CAEDF0C008C271F /* FBSessionCommands.m */; }; + 641EE6192240C5CA00173FCB /* FBConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A21CF7A43900275851 /* FBConfiguration.m */; }; + 641EE61A2240C5CA00173FCB /* FBElementCache.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC088E41CB56AC000B65968 /* FBElementCache.m */; }; + 641EE61B2240C5CA00173FCB /* FBPasteboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 71930C4120662E1F00D3AFEC /* FBPasteboard.m */; }; + 641EE61C2240C5CA00173FCB /* FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26931CF2379700F8B5FF /* FBAlert.m */; }; + 641EE61D2240C5CA00173FCB /* FBElementCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7571CAEDF0C008C271F /* FBElementCommands.m */; }; + 641EE61E2240C5CA00173FCB /* FBExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */; }; + 641EE61F2240C5CA00173FCB /* FBXCodeCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */; }; + 641EE6212240C5CA00173FCB /* FBElementTypeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */; }; + 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; + 641EE6242240C5CA00173FCB /* FBXCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */; }; + 641EE6262240C5CA00173FCB /* FBMathUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1888391DA661C400307AA8 /* FBMathUtils.m */; }; + 641EE6272240C5CA00173FCB /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; + 641EE6312240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6322240C5CA00173FCB /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; }; + 641EE6332240C5CA00173FCB /* XCTestPrivateSymbols.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */; }; + 641EE6342240C5CA00173FCB /* XCUIElement+FBTyping.h in Headers */ = {isa = PBXBuildFile; fileRef = AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6352240C5CA00173FCB /* XCUIElement+FBUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6362240C5CA00173FCB /* XCUIElement+FBScrolling.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6372240C5CA00173FCB /* XCSourceCodeTreeNode.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC51E3B77D600A02D78 /* XCSourceCodeTreeNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6382240C5CA00173FCB /* XCPointerEventPath.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC31E3B77D600A02D78 /* XCPointerEventPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6392240C5CA00173FCB /* FBRouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE63A2240C5CA00173FCB /* XCTest.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCF1E3B77D600A02D78 /* XCTest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE63B2240C5CA00173FCB /* FBAlertsMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */; }; + 641EE63D2240C5CA00173FCB /* FBSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78A1CAEDF0C008C271F /* FBSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE63E2240C5CA00173FCB /* _XCTestImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9E1E3B77D600A02D78 /* _XCTestImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE63F2240C5CA00173FCB /* FBTouchActionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */; }; + 641EE6402240C5CA00173FCB /* FBTouchIDCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7621CAEDF0C008C271F /* FBTouchIDCommands.h */; }; + 641EE6412240C5CA00173FCB /* XCUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF91E3B77D600A02D78 /* XCUIApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6422240C5CA00173FCB /* FBCustomCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */; }; + 641EE6432240C5CA00173FCB /* _XCTestCaseInterruptionException.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9C1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6442240C5CA00173FCB /* FBOrientationCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB75C1CAEDF0C008C271F /* FBOrientationCommands.h */; }; + 641EE6452240C5CA00173FCB /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6462240C5CA00173FCB /* XCTRunnerIDESession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF01E3B77D600A02D78 /* XCTRunnerIDESession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6472240C5CA00173FCB /* FBRouteRequest-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */; }; + 641EE6482240C5CA00173FCB /* XCTTestRunSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF11E3B77D600A02D78 /* XCTTestRunSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6492240C5CA00173FCB /* XCTestProbe.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE31E3B77D600A02D78 /* XCTestProbe.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE64A2240C5CA00173FCB /* XCApplicationQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE64B2240C5CA00173FCB /* XCTAsyncActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCB1E3B77D600A02D78 /* XCTAsyncActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE64C2240C5CA00173FCB /* XCTestMisuseObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDF1E3B77D600A02D78 /* XCTestMisuseObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE64D2240C5CA00173FCB /* XCTRunnerDaemonSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEF1E3B77D600A02D78 /* XCTRunnerDaemonSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE64F2240C5CA00173FCB /* XCTestExpectationWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDA1E3B77D600A02D78 /* XCTestExpectationWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6502240C5CA00173FCB /* UIGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAD1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6512240C5CA00173FCB /* XCKeyboardKeyMap.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBF1E3B77D600A02D78 /* XCKeyboardKeyMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6522240C5CA00173FCB /* XCTNSPredicateExpectationObject-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEC1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6532240C5CA00173FCB /* WebDriverAgentLib.h in Headers */ = {isa = PBXBuildFile; fileRef = EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6542240C5CA00173FCB /* FBFindElementCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7581CAEDF0C008C271F /* FBFindElementCommands.h */; }; + 641EE6552240C5CA00173FCB /* XCTestRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE41E3B77D600A02D78 /* XCTestRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6562240C5CA00173FCB /* FBWebServer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78C1CAEDF0C008C271F /* FBWebServer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6572240C5CA00173FCB /* FBScreenshotCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB75E1CAEDF0C008C271F /* FBScreenshotCommands.h */; }; + 641EE6582240C5CA00173FCB /* _XCKVOExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC991E3B77D600A02D78 /* _XCKVOExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6592240C5CA00173FCB /* NSString+FBVisualLength.h in Headers */ = {isa = PBXBuildFile; fileRef = EE0D1F5F1EBCDCF7006A3123 /* NSString+FBVisualLength.h */; }; + 641EE65A2240C5CA00173FCB /* FBXCTestDaemonsProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD791E3B80C000A02D78 /* FBXCTestDaemonsProxy.h */; }; + 641EE65B2240C5CA00173FCB /* XCUIElementHitPointCoordinate.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD011E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE65C2240C5CA00173FCB /* XCTDarwinNotificationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCE1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE65D2240C5CA00173FCB /* XCTRunnerAutomationSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEE1E3B77D600A02D78 /* XCTRunnerAutomationSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE65F2240C5CA00173FCB /* XCSourceCodeTreeNodeEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC61E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6602240C5CA00173FCB /* XCUIElement+FBIsVisible.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6622240C5CA00173FCB /* FBResponsePayload.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7821CAEDF0C008C271F /* FBResponsePayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */; }; + 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */; }; + 641EE6652240C5CA00173FCB /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAE1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6662240C5CA00173FCB /* XCTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD01E3B77D600A02D78 /* XCTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6672240C5CA00173FCB /* XCSymbolicatorHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC81E3B77D600A02D78 /* XCSymbolicatorHolder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6682240C5CA00173FCB /* XCUIApplicationImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFA1E3B77D600A02D78 /* XCUIApplicationImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6692240C5CA00173FCB /* UIPanGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAF1E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE66A2240C5CA00173FCB /* NSExpression+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71555A3B1DEC460A007D4A8B /* NSExpression+FBFormat.h */; }; + 641EE66B2240C5CA00173FCB /* _XCTestCaseImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9B1E3B77D600A02D78 /* _XCTestCaseImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE66C2240C5CA00173FCB /* UIPinchGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB01E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE66D2240C5CA00173FCB /* XCTestManager_TestsInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDE1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE66E2240C5CA00173FCB /* XCUIApplication+FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */; }; + 641EE6702240C5CA00173FCB /* FBMathUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE1888381DA661C400307AA8 /* FBMathUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6712240C5CA00173FCB /* UISwipeGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB11E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6722240C5CA00173FCB /* FBElementUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */; }; + 641EE6732240C5CA00173FCB /* FBDebugCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7541CAEDF0C008C271F /* FBDebugCommands.h */; }; + 641EE6742240C5CA00173FCB /* XCTestSuite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE51E3B77D600A02D78 /* XCTestSuite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6752240C5CA00173FCB /* XCUICoordinate.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFC1E3B77D600A02D78 /* XCUICoordinate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6762240C5CA00173FCB /* XCTNSPredicateExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEB1E3B77D600A02D78 /* XCTNSPredicateExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6772240C5CA00173FCB /* XCTestObservationCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE11E3B77D600A02D78 /* XCTestObservationCenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6782240C5CA00173FCB /* XCTNSNotificationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEA1E3B77D600A02D78 /* XCTNSNotificationExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6792240C5CA00173FCB /* XCUIRecorderNodeFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD041E3B77D600A02D78 /* XCUIRecorderNodeFinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67A2240C5CA00173FCB /* XCUIElement+FBAccessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7451CAEDF0C008C271F /* XCUIElement+FBAccessibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67B2240C5CA00173FCB /* XCUIRecorderUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD071E3B77D600A02D78 /* XCUIRecorderUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67C2240C5CA00173FCB /* XCTestCaseRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD11E3B77D600A02D78 /* XCTestCaseRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67D2240C5CA00173FCB /* XCTestConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD31E3B77D600A02D78 /* XCTestConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67E2240C5CA00173FCB /* _XCTDarwinNotificationExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9A1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE67F2240C5CA00173FCB /* XCTestExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD81E3B77D600A02D78 /* XCTestExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6802240C5CA00173FCB /* FBElementTypeTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6812240C5CA00173FCB /* FBXCAXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */; }; + 641EE6822240C5CA00173FCB /* FBElementCache.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB77B1CAEDF0C008C271F /* FBElementCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6832240C5CA00173FCB /* XCTMetric.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE91E3B77D600A02D78 /* XCTMetric.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6842240C5CA00173FCB /* XCTestContextScope.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD51E3B77D600A02D78 /* XCTestContextScope.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6852240C5CA00173FCB /* XCUIElement+FBClassChain.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */; }; + 641EE6862240C5CA00173FCB /* FBResponseJSONPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7801CAEDF0C008C271F /* FBResponseJSONPayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6872240C5CA00173FCB /* XCTAutomationTarget-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCC1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6882240C5CA00173FCB /* FBElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7791CAEDF0C008C271F /* FBElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6892240C5CA00173FCB /* XCTAXClient-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCD1E3B77D600A02D78 /* XCTAXClient-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE68B2240C5CA00173FCB /* FBExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EEC088E61CB56DA400B65968 /* FBExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE68C2240C5CA00173FCB /* FBRoute.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7841CAEDF0C008C271F /* FBRoute.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE68D2240C5CA00173FCB /* XCTestDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD61E3B77D600A02D78 /* XCTestDriver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE68E2240C5CA00173FCB /* _XCTNSNotificationExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA11E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE68F2240C5CA00173FCB /* XCSynthesizedEventRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC91E3B77D600A02D78 /* XCSynthesizedEventRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6922240C5CA00173FCB /* XCTWaiterDelegatePrivate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF61E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6932240C5CA00173FCB /* XCTestManager_IDEInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDC1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6942240C5CA00173FCB /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6952240C5CA00173FCB /* XCUIRecorderTimingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD061E3B77D600A02D78 /* XCUIRecorderTimingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6962240C5CA00173FCB /* XCApplicationMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB61E3B77D600A02D78 /* XCApplicationMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6972240C5CA00173FCB /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6982240C5CA00173FCB /* FBRuntimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7911CAEDF0C008C271F /* FBRuntimeUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6992240C5CA00173FCB /* XCUIElement+FBPickerWheel.h in Headers */ = {isa = PBXBuildFile; fileRef = 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */; }; + 641EE69A2240C5CA00173FCB /* XCTestObservation-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE01E3B77D600A02D78 /* XCTestObservation-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE69B2240C5CA00173FCB /* _XCTNSPredicateExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA21E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE69C2240C5CA00173FCB /* FBElementCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7561CAEDF0C008C271F /* FBElementCommands.h */; }; + 641EE69F2240C5CA00173FCB /* FBTCPSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 715557D1211DBCE700613B26 /* FBTCPSocket.h */; }; + 641EE6A02240C5CA00173FCB /* XCUIElement+FBUID.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */; }; + 641EE6A12240C5CA00173FCB /* XCSymbolicationRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC71E3B77D600A02D78 /* XCSymbolicationRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6A22240C5CA00173FCB /* XCUIDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6A32240C5CA00173FCB /* XCUIApplication+FBTouchAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */; }; + 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7601CAEDF0C008C271F /* FBSessionCommands.h */; }; + 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */; }; + 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */; }; + 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */ = {isa = PBXBuildFile; fileRef = 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */; }; + 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6AB2240C5CA00173FCB /* FBAlertViewCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */; }; + 641EE6AC2240C5CA00173FCB /* XCTWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF41E3B77D600A02D78 /* XCTWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6AD2240C5CA00173FCB /* XCTWaiterManagement-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF71E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6AF2240C5CA00173FCB /* XCTestContext.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD41E3B77D600A02D78 /* XCTestContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B12240C5CA00173FCB /* XCTWaiterDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF51E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B22240C5CA00173FCB /* _XCTestExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9D1E3B77D600A02D78 /* _XCTestExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B32240C5CA00173FCB /* XCAXClient_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB81E3B77D600A02D78 /* XCAXClient_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B42240C5CA00173FCB /* XCTWaiterManager.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF81E3B77D600A02D78 /* XCTWaiterManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B52240C5CA00173FCB /* XCTestDriverInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD71E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B62240C5CA00173FCB /* _XCTestSuiteImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA01E3B77D600A02D78 /* _XCTestSuiteImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B72240C5CA00173FCB /* FBBaseActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */; }; + 641EE6B82240C5CA00173FCB /* FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26921CF2379700F8B5FF /* FBAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6B92240C5CA00173FCB /* XCUIElementQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD021E3B77D600A02D78 /* XCUIElementQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BA2240C5CA00173FCB /* XCPointerEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC21E3B77D600A02D78 /* XCPointerEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BB2240C5CA00173FCB /* XCSourceCodeRecording.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC41E3B77D600A02D78 /* XCSourceCodeRecording.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BC2240C5CA00173FCB /* FBRunLoopSpinner.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BD2240C5CA00173FCB /* FBErrorBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BE2240C5CA00173FCB /* XCApplicationMonitor_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB51E3B77D600A02D78 /* XCApplicationMonitor_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6BF2240C5CA00173FCB /* FBKeyboard.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18641CDE734B00DE4205 /* FBKeyboard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C02240C5CA00173FCB /* XCUIApplication+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C12240C5CA00173FCB /* _XCTestObservationCenterImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9F1E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C22240C5CA00173FCB /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C32240C5CA00173FCB /* FBClassChainQueryParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */; }; + 641EE6C42240C5CA00173FCB /* FBMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A51CF7A43900275851 /* FBMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C52240C5CA00173FCB /* XCTestExpectationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD91E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C62240C5CA00173FCB /* XCTUIApplicationMonitor-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF31E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C82240C5CA00173FCB /* XCTKVOExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE81E3B77D600A02D78 /* XCTKVOExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6C92240C5CA00173FCB /* XCUIDevice+FBRotation.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CA2240C5CA00173FCB /* XCEventGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBD1E3B77D600A02D78 /* XCEventGenerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CB2240C5CA00173FCB /* FBConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A11CF7A43900275851 /* FBConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CC2240C5CA00173FCB /* XCTestSuiteRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE61E3B77D600A02D78 /* XCTestSuiteRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CD2240C5CA00173FCB /* XCUIElementAsynchronousHandlerWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFF1E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CE2240C5CA00173FCB /* XCTestLog.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDB1E3B77D600A02D78 /* XCTestLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6CF2240C5CA00173FCB /* UITapGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB21E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D02240C5CA00173FCB /* XCDebugLogDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB91E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D12240C5CA00173FCB /* NSString-XCTAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAB1E3B77D600A02D78 /* NSString-XCTAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D22240C5CA00173FCB /* XCTestWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE71E3B77D600A02D78 /* XCTestWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D32240C5CA00173FCB /* FBImageUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 7150348521A6DAD600A0F4BA /* FBImageUtils.h */; }; + 641EE6D42240C5CA00173FCB /* NSValue-XCTestAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAC1E3B77D600A02D78 /* NSValue-XCTestAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D52240C5CA00173FCB /* _XCTWaiterImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA31E3B77D600A02D78 /* _XCTWaiterImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D62240C5CA00173FCB /* FBLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A31CF7A43900275851 /* FBLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D72240C5CA00173FCB /* XCTestObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE21E3B77D600A02D78 /* XCTestObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; + 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6DE2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6DF2240C5CA00173FCB /* FBMjpegServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7155D701211DCEF400166C20 /* FBMjpegServer.h */; }; + 641EE6E02240C5CA00173FCB /* XCUIRecorderNodeFinderMatch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD051E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E12240C5CA00173FCB /* XCUIApplicationProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFB1E3B77D600A02D78 /* XCUIApplicationProcess.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E22240C5CA00173FCB /* FBW3CActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */; }; + 641EE6E32240C5CA00173FCB /* CDStructures.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA41E3B77D600A02D78 /* CDStructures.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E42240C5CA00173FCB /* XCKeyboardLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC01E3B77D600A02D78 /* XCKeyboardLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E52240C5CA00173FCB /* XCTAsyncActivity-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCA1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E62240C5CA00173FCB /* XCActivityRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB41E3B77D600A02D78 /* XCActivityRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E72240C5CA00173FCB /* XCUIElement+FBFind.h in Headers */ = {isa = PBXBuildFile; fileRef = EEBBD4891D47746D00656A81 /* XCUIElement+FBFind.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E82240C5CA00173FCB /* XCTestManager_ManagerInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDD1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6E92240C5CA00173FCB /* FBFailureProofTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6EA2240C5CA00173FCB /* XCTTestRunSessionDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF21E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6EB2240C5CA00173FCB /* XCTestCaseSuite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD21E3B77D600A02D78 /* XCTestCaseSuite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6EC2240C5CA00173FCB /* _XCInternalTestRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC981E3B77D600A02D78 /* _XCInternalTestRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6ED2240C5CA00173FCB /* FBXPath-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */; }; + 641EE6EE2240C5CA00173FCB /* XCKeyMappingPath.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC11E3B77D600A02D78 /* XCKeyMappingPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 641EE6FC2240C5FD00173FCB /* WebDriverAgentLib_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */; }; + 641EE6FD2240C61D00173FCB /* WebDriverAgentLib_tvOS.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 641EE7052240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */; }; + 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */; }; + 641EE7082240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */ = {isa = PBXBuildFile; fileRef = 641EE7072240CDEB00173FCB /* XCUIElement+FBTVFocuse.m */; }; + 641EE7092240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */ = {isa = PBXBuildFile; fileRef = 641EE7072240CDEB00173FCB /* XCUIElement+FBTVFocuse.m */; }; + 641EE70B2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE70A2240CE2D00173FCB /* FBTVNavigationTracker.h */; }; + 641EE70C2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE70A2240CE2D00173FCB /* FBTVNavigationTracker.h */; }; + 641EE70E2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 641EE70D2240CE4800173FCB /* FBTVNavigationTracker.m */; }; + 641EE70F2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 641EE70D2240CE4800173FCB /* FBTVNavigationTracker.m */; }; + 644D9CCE230E1F1A00C90459 /* FBConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 644D9CCD230E1F1A00C90459 /* FBConfigurationTests.m */; }; + 648C10AB22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 648C10AA22AAAD9C00B81B9A /* UIKeyboardImpl.h */; }; + 648C10AC22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 648C10AA22AAAD9C00B81B9A /* UIKeyboardImpl.h */; }; + 648C10AF22AAAE4000B81B9A /* TIPreferencesController.h in Headers */ = {isa = PBXBuildFile; fileRef = 648C10AE22AAAE4000B81B9A /* TIPreferencesController.h */; }; + 648C10B022AAAE4000B81B9A /* TIPreferencesController.h in Headers */ = {isa = PBXBuildFile; fileRef = 648C10AE22AAAE4000B81B9A /* TIPreferencesController.h */; }; + 6496A5D9230D6EB30087F8CB /* AXSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 6496A5D8230D6EB30087F8CB /* AXSettings.h */; }; + 6496A5DA230D6EB30087F8CB /* AXSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 6496A5D8230D6EB30087F8CB /* AXSettings.h */; }; + 64B264FE228C50E0002A5025 /* WebDriverAgentLib_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */; }; + 64B26504228C5299002A5025 /* FBTVNavigationTrackerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B264F3228C5098002A5025 /* FBTVNavigationTrackerTests.m */; }; + 64B26508228C5514002A5025 /* XCUIElementDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B26507228C5514002A5025 /* XCUIElementDouble.m */; }; + 64B2650A228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */; }; + 64B2650B228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */; }; + 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */; }; + 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; + 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; + 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */; }; + 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; + 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; + 71241D7B1FAE3D2500B9559F /* FBTouchActionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */; }; + 71241D7C1FAE3D2500B9559F /* FBTouchActionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */; }; + 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */; }; + 71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */; }; + 712A0C851DA3E459007D02E5 /* FBXPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 712A0C841DA3E459007D02E5 /* FBXPathTests.m */; }; + 712A0C871DA3E55D007D02E5 /* FBXPath-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */; }; + 713352FD26CEF31D00523CBC /* FBLRUCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 713352FC26CEF31D00523CBC /* FBLRUCacheTests.m */; }; + 7136A4791E8918E60024FC3D /* XCUIElement+FBPickerWheel.h in Headers */ = {isa = PBXBuildFile; fileRef = 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */; }; + 7136A47A1E8918E60024FC3D /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; + 7136C0F9243A182400921C76 /* FBW3CTypeActionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */; }; + 7139145A1DF01989005896C2 /* XCUIElementHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 713914591DF01989005896C2 /* XCUIElementHelpersTests.m */; }; + 7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7139145B1DF01A12005896C2 /* NSExpressionFBFormatTests.m */; }; + 713AE575243A53BE0000D657 /* FBW3CActionsHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 713AE573243A53BE0000D657 /* FBW3CActionsHelpers.h */; }; + 713AE576243A53BE0000D657 /* FBW3CActionsHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 713AE574243A53BE0000D657 /* FBW3CActionsHelpers.m */; }; + 713C6DCF1DDC772A00285B92 /* FBElementUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */; }; + 713C6DD01DDC772A00285B92 /* FBElementUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */; }; + 714097431FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */; }; + 7140974B1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */; }; + 7140974C1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */; }; + 7140974E1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */; }; + 71414ED42670A1EE003A8C5D /* LRUCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 71414ED02670A1ED003A8C5D /* LRUCache.h */; }; + 71414ED52670A1EE003A8C5D /* LRUCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 71414ED02670A1ED003A8C5D /* LRUCache.h */; }; + 71414ED62670A1EE003A8C5D /* LRUCacheNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 71414ED12670A1ED003A8C5D /* LRUCacheNode.h */; }; + 71414ED72670A1EE003A8C5D /* LRUCacheNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 71414ED12670A1ED003A8C5D /* LRUCacheNode.h */; }; + 71414ED82670A1EE003A8C5D /* LRUCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 71414ED22670A1ED003A8C5D /* LRUCache.m */; }; + 71414ED92670A1EE003A8C5D /* LRUCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 71414ED22670A1ED003A8C5D /* LRUCache.m */; }; + 71414EDA2670A1EE003A8C5D /* LRUCacheNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 71414ED32670A1ED003A8C5D /* LRUCacheNode.m */; }; + 71414EDB2670A1EE003A8C5D /* LRUCacheNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 71414ED32670A1ED003A8C5D /* LRUCacheNode.m */; }; + 714801D11FA9D9FA00DC5997 /* FBSDKVersionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 714801D01FA9D9FA00DC5997 /* FBSDKVersionTests.m */; }; + 714D88CC2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */; }; + 714D88CD2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */; }; + 714D88CE2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */; }; + 714D88CF2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */; }; + 714E14B829805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + 714E14B929805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + 714E14BA29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */; }; + 714E14BB29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */; }; + 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */; }; + 714EAA0E2673FDFE005C5B47 /* FBCapabilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */; }; + 714EAA0F2673FDFE005C5B47 /* FBCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */; }; + 714EAA102673FDFE005C5B47 /* FBCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */; }; + 7150348721A6DAD600A0F4BA /* FBImageUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 7150348521A6DAD600A0F4BA /* FBImageUtils.h */; }; + 7150348821A6DAD600A0F4BA /* FBImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 7150348621A6DAD600A0F4BA /* FBImageUtils.m */; }; + 7150FFF722476B3A00B2EE28 /* FBForceTouchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7A20C57320004D4925 /* FBForceTouchTests.m */; }; + 7152EB301F41F9960047EEFF /* FBSessionIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7152EB2F1F41F9960047EEFF /* FBSessionIntegrationTests.m */; }; + 715557D3211DBCE700613B26 /* FBTCPSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 715557D1211DBCE700613B26 /* FBTCPSocket.h */; }; + 715557D4211DBCE700613B26 /* FBTCPSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 715557D2211DBCE700613B26 /* FBTCPSocket.m */; }; + 71555A3D1DEC460A007D4A8B /* NSExpression+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71555A3B1DEC460A007D4A8B /* NSExpression+FBFormat.h */; }; + 71555A3E1DEC460A007D4A8B /* NSExpression+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71555A3C1DEC460A007D4A8B /* NSExpression+FBFormat.m */; }; + 7155B40E224D5A850042A993 /* libAccessibility.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9343224D53DF004B8542 /* libAccessibility.tbd */; }; + 7155B414224D5B170042A993 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9342224D53A1004B8542 /* XCTest.framework */; }; + 7155B41B224D5B5A0042A993 /* libAccessibility.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7155B41A224D5B480042A993 /* libAccessibility.tbd */; }; + 7155B41C224D5B5D0042A993 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7155B419224D5B460042A993 /* libxml2.tbd */; }; + 7155B424224D5BA10042A993 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7155B423224D5B980042A993 /* XCTest.framework */; }; + 7155D703211DCEF400166C20 /* FBMjpegServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7155D701211DCEF400166C20 /* FBMjpegServer.h */; }; + 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; + 7157B291221DADD2001C348C /* FBXCAXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */; }; + 7157B292221DADD2001C348C /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; + 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; + 715AFAC11FFA29180053896D /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; }; + 715AFAC21FFA29180053896D /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; + 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */; }; + 715D554B2229891B00524509 /* FBExceptionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715D554A2229891B00524509 /* FBExceptionHandlerTests.m */; }; + 715D5773224DE02E00DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 715D5774224DE05400DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 715D5775224DE05C00DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 715D5776224DE06500DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 716C9346224D540C004B8542 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 716C9347224D540C004B8542 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; }; + 716C9DFA27315D21005AD475 /* FBReflectionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 716C9DF827315D21005AD475 /* FBReflectionUtils.h */; }; + 716C9DFB27315D21005AD475 /* FBReflectionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 716C9DF827315D21005AD475 /* FBReflectionUtils.h */; }; + 716C9DFC27315D21005AD475 /* FBReflectionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 716C9DF927315D21005AD475 /* FBReflectionUtils.m */; }; + 716C9DFD27315D21005AD475 /* FBReflectionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 716C9DF927315D21005AD475 /* FBReflectionUtils.m */; }; + 716C9E0027315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 716C9DFE27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h */; }; + 716C9E0127315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 716C9DFE27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h */; }; + 716C9E0227315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 716C9DFF27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m */; }; + 716C9E0327315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 716C9DFF27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m */; }; + 716E0BCE1E917E810087A825 /* NSString+FBXMLSafeString.h in Headers */ = {isa = PBXBuildFile; fileRef = 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */; }; + 716E0BCF1E917E810087A825 /* NSString+FBXMLSafeString.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */; }; + 716E0BD11E917F260087A825 /* FBXMLSafeStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */; }; + 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; + 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */; }; + 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */; }; + 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; + 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; + 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; + 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; + 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; + 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; + 718226D02587443700661B83 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */; }; + 718226D12587443700661B83 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */; }; + 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA224913C210060D7EB /* HTTPResponseProxy.h */; }; + 7182270B258744A700661B83 /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; }; + 71822714258744A900661B83 /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCAA24913C220060D7EB /* RouteRequest.h */; }; + 7182271D258744AB00661B83 /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA124913C210060D7EB /* RouteResponse.h */; }; + 71822726258744AE00661B83 /* RoutingConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA524913C210060D7EB /* RoutingConnection.h */; }; + 7182272F258744B000661B83 /* RoutingHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA724913C210060D7EB /* RoutingHTTPServer.h */; }; + 71822738258744B800661B83 /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC89249131D30060D7EB /* HTTPConnection.h */; }; + 71822741258744BB00661B83 /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8D249131D30060D7EB /* HTTPLogging.h */; }; + 7182274A258744BE00661B83 /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC87249131D30060D7EB /* HTTPMessage.h */; }; + 71822753258744C100661B83 /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8F249131D40060D7EB /* HTTPResponse.h */; }; + 7182275C258744C300661B83 /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8B249131D30060D7EB /* HTTPServer.h */; }; + 71822765258744C700661B83 /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC60249131890060D7EB /* HTTPDataResponse.h */; }; + 7182276E258744C900661B83 /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC59249131880060D7EB /* HTTPErrorResponse.h */; }; + 71822777258744CE00661B83 /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7D249131B00060D7EB /* DDNumber.h */; }; + 71822780258744D000661B83 /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7B249131B00060D7EB /* DDRange.h */; }; + 718F49C8230844330045FE8B /* FBProtocolHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */; }; + 718F49C923087ACF0045FE8B /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; + 718F49CA23087AD30045FE8B /* FBProtocolHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */; }; + 718F49CB23087B040045FE8B /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; + 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; + 71930C4320662E1F00D3AFEC /* FBPasteboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 71930C4120662E1F00D3AFEC /* FBPasteboard.m */; }; + 71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71930C462066434000D3AFEC /* FBPasteboardTests.m */; }; + 719CD8F82126C78F00C7D0C2 /* FBAlertsMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */; }; + 719CD8F92126C78F00C7D0C2 /* FBAlertsMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */; }; + 719CD8FC2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */; }; + 719CD8FD2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */; }; + 719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FE2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m */; }; + 719DCF152601EAFB000E765F /* FBNotificationsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 719DCF132601EAFB000E765F /* FBNotificationsHelper.h */; }; + 719DCF162601EAFB000E765F /* FBNotificationsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 719DCF132601EAFB000E765F /* FBNotificationsHelper.h */; }; + 719DCF172601EAFB000E765F /* FBNotificationsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 719DCF142601EAFB000E765F /* FBNotificationsHelper.m */; }; + 719DCF182601EAFB000E765F /* FBNotificationsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 719DCF142601EAFB000E765F /* FBNotificationsHelper.m */; }; + 719FF5B91DAD21F5008E0099 /* FBElementUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 719FF5B81DAD21F5008E0099 /* FBElementUtilitiesTests.m */; }; + 71A224E51DE2F56600844D55 /* NSPredicate+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */; }; + 71A224E61DE2F56600844D55 /* NSPredicate+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */; }; + 71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */; }; + 71A5C67329A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71A5C67429A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */; }; + 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */; }; + 71A7EAF51E20516B001DA4F2 /* XCUIElement+FBClassChain.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */; }; + 71A7EAF61E20516B001DA4F2 /* XCUIElement+FBClassChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */; }; + 71A7EAF91E224648001DA4F2 /* FBClassChainQueryParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */; }; + 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */; }; + 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */; }; + 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */; }; + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; + 71B155DA23070ECF00646AFB /* FBHTTPStatusCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; + 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; + 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */; }; + 71B49EC71ED1A58100D51AD6 /* XCUIElement+FBUID.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */; }; + 71B49EC81ED1A58100D51AD6 /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; + 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */; }; + 71BB58E12B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */; }; + 71BB58E22B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */; }; + 71BB58E32B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E52B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E82B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */; }; + 71BB58E92B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */; }; + 71BB58EA2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EB2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EC2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EF2B96511800CB9BFE /* FBVideoCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */; }; + 71BB58F02B96511800CB9BFE /* FBVideoCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */; }; + 71BB58F12B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F22B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F32B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F62B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */; }; + 71BB58F72B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */; }; + 71BB58F82B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; + 71BB58F92B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; + 71BB58FA2B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; + 71BD20731F86116100B36EC2 /* XCUIApplication+FBTouchAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */; }; + 71BD20741F86116100B36EC2 /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; + 71C8E55125399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */; }; + 71C8E55225399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */; }; + 71C8E55325399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */; }; + 71C8E55425399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */; }; + 71C9EAAC25E8415A00470CD8 /* FBScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */; }; + 71C9EAAD25E8415A00470CD8 /* FBScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */; }; + 71C9EAAE25E8415A00470CD8 /* FBScreenshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C9EAAB25E8415A00470CD8 /* FBScreenshot.m */; }; + 71C9EAAF25E8415A00470CD8 /* FBScreenshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C9EAAB25E8415A00470CD8 /* FBScreenshot.m */; }; + 71D04DC825356C43008A052C /* XCUIElement+FBCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D04DC625356C43008A052C /* XCUIElement+FBCaching.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71D04DC925356C43008A052C /* XCUIElement+FBCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D04DC625356C43008A052C /* XCUIElement+FBCaching.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71D04DCA25356C43008A052C /* XCUIElement+FBCaching.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D04DC725356C43008A052C /* XCUIElement+FBCaching.m */; }; + 71D04DCB25356C43008A052C /* XCUIElement+FBCaching.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D04DC725356C43008A052C /* XCUIElement+FBCaching.m */; }; + 71D3B3D5267FC7260076473D /* XCUIElement+FBResolve.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */; }; + 71D3B3D6267FC7260076473D /* XCUIElement+FBResolve.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */; }; + 71D3B3D7267FC7260076473D /* XCUIElement+FBResolve.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D3B3D4267FC7260076473D /* XCUIElement+FBResolve.m */; }; + 71D3B3D8267FC7260076473D /* XCUIElement+FBResolve.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D3B3D4267FC7260076473D /* XCUIElement+FBResolve.m */; }; + 71D475C22538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */; }; + 71D475C32538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */; }; + 71D475C42538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */; }; + 71D475C52538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */; }; + 71E75E6D254824230099FC87 /* XCUIElementQuery+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E75E6B254824230099FC87 /* XCUIElementQuery+FBHelpers.h */; }; + 71E75E6E254824230099FC87 /* XCUIElementQuery+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E75E6B254824230099FC87 /* XCUIElementQuery+FBHelpers.h */; }; + 71E75E6F254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */; }; + 71E75E70254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */; }; + 71F3E7D425417FF400E0C22B /* FBSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D225417FF400E0C22B /* FBSettings.h */; }; + 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D225417FF400E0C22B /* FBSettings.h */; }; + 71F3E7D625417FF400E0C22B /* FBSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D325417FF400E0C22B /* FBSettings.m */; }; + 71F3E7D725417FF400E0C22B /* FBSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D325417FF400E0C22B /* FBSettings.m */; }; + 71F5BE23252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */; }; + 71F5BE24252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */; }; + 71F5BE25252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */; }; + 71F5BE26252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */; }; + 71F5BE34252E5B2200EE9EBA /* FBElementSwipingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */; }; + 71F5BE4F252F14EB00EE9EBA /* FBExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */; }; + 71F5BE50252F14EB00EE9EBA /* FBExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */; }; + 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; + 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; + AD35D06C1CF1C35500870A75 /* WebDriverAgentLib.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26921CF2379700F8B5FF /* FBAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD6C26951CF2379700F8B5FF /* FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26931CF2379700F8B5FF /* FBAlert.m */; }; + AD6C26981CF2481700F8B5FF /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD6C26991CF2481700F8B5FF /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; }; + AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD6C269D1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */; }; + AD76723D1D6B7CC000610457 /* XCUIElement+FBTyping.h in Headers */ = {isa = PBXBuildFile; fileRef = AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD76723E1D6B7CC000610457 /* XCUIElement+FBTyping.m in Sources */ = {isa = PBXBuildFile; fileRef = AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */; }; + AD8D96F21D3C12990061268E /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; + ADBC39941D0782CD00327304 /* FBElementCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC39931D0782CD00327304 /* FBElementCacheTests.m */; }; + ADBC39981D07842800327304 /* XCUIElementDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC39971D07842800327304 /* XCUIElementDouble.m */; }; + ADDA07241D6BB2BF001700AC /* FBScrollViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */; }; + ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */; }; + B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; + B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; + B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; + B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; + C845206222D5E79400EA68CB /* FBUnattachedAppLauncher.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */; }; + C845206322D5E79700EA68CB /* FBUnattachedAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */; }; + C8FB547422D3949C00B69954 /* LSApplicationWorkspace.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */; }; + C8FB547922D4C1FC00B69954 /* FBUnattachedAppLauncher.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */; }; + C8FB547A22D4C1FC00B69954 /* FBUnattachedAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */; }; + E444DC65249131890060D7EB /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC59249131880060D7EB /* HTTPErrorResponse.h */; }; + E444DC67249131890060D7EB /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC5B249131880060D7EB /* HTTPDataResponse.m */; }; + E444DC6C249131890060D7EB /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC60249131890060D7EB /* HTTPDataResponse.h */; }; + E444DC6D249131890060D7EB /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC61249131890060D7EB /* HTTPErrorResponse.m */; }; + E444DC81249131B10060D7EB /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7B249131B00060D7EB /* DDRange.h */; }; + E444DC83249131B10060D7EB /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7D249131B00060D7EB /* DDNumber.h */; }; + E444DC84249131B10060D7EB /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7E249131B00060D7EB /* DDRange.m */; }; + E444DC85249131B10060D7EB /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7F249131B00060D7EB /* DDNumber.m */; }; + E444DC93249131D40060D7EB /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC87249131D30060D7EB /* HTTPMessage.h */; }; + E444DC95249131D40060D7EB /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC89249131D30060D7EB /* HTTPConnection.h */; }; + E444DC97249131D40060D7EB /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8B249131D30060D7EB /* HTTPServer.h */; }; + E444DC98249131D40060D7EB /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC8C249131D30060D7EB /* HTTPConnection.m */; }; + E444DC99249131D40060D7EB /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8D249131D30060D7EB /* HTTPLogging.h */; }; + E444DC9B249131D40060D7EB /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8F249131D40060D7EB /* HTTPResponse.h */; }; + E444DC9C249131D40060D7EB /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC90249131D40060D7EB /* HTTPServer.m */; }; + E444DC9D249131D40060D7EB /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC91249131D40060D7EB /* HTTPMessage.m */; }; + E444DCAB24913C220060D7EB /* HTTPResponseProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */; }; + E444DCAC24913C220060D7EB /* Route.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA024913C210060D7EB /* Route.m */; }; + E444DCAD24913C220060D7EB /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA124913C210060D7EB /* RouteResponse.h */; }; + E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA224913C210060D7EB /* HTTPResponseProxy.h */; }; + E444DCAF24913C220060D7EB /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; }; + E444DCB024913C220060D7EB /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA424913C210060D7EB /* RouteResponse.m */; }; + E444DCB124913C220060D7EB /* RoutingConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA524913C210060D7EB /* RoutingConnection.h */; }; + E444DCB224913C220060D7EB /* RoutingConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA624913C210060D7EB /* RoutingConnection.m */; }; + E444DCB324913C220060D7EB /* RoutingHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA724913C210060D7EB /* RoutingHTTPServer.h */; }; + E444DCB424913C220060D7EB /* RoutingHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA824913C220060D7EB /* RoutingHTTPServer.m */; }; + E444DCB524913C220060D7EB /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA924913C220060D7EB /* RouteRequest.m */; }; + E444DCB624913C220060D7EB /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCAA24913C220060D7EB /* RouteRequest.h */; }; + E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */; }; + E444DCBE24917A5E0060D7EB /* Route.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA024913C210060D7EB /* Route.m */; }; + E444DCC024917A5E0060D7EB /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA924913C220060D7EB /* RouteRequest.m */; }; + E444DCC224917A5E0060D7EB /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA424913C210060D7EB /* RouteResponse.m */; }; + E444DCC424917A5E0060D7EB /* RoutingConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA624913C210060D7EB /* RoutingConnection.m */; }; + E444DCC624917A5E0060D7EB /* RoutingHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA824913C220060D7EB /* RoutingHTTPServer.m */; }; + E444DCC824917A5E0060D7EB /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC8C249131D30060D7EB /* HTTPConnection.m */; }; + E444DCCB24917A5E0060D7EB /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC91249131D40060D7EB /* HTTPMessage.m */; }; + E444DCCE24917A5E0060D7EB /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC90249131D40060D7EB /* HTTPServer.m */; }; + E444DCD024917A5E0060D7EB /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC5B249131880060D7EB /* HTTPDataResponse.m */; }; + E444DCD224917A5E0060D7EB /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC61249131890060D7EB /* HTTPErrorResponse.m */; }; + E444DCD424917A5E0060D7EB /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7F249131B00060D7EB /* DDNumber.m */; }; + E444DCD624917A5E0060D7EB /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7E249131B00060D7EB /* DDRange.m */; }; + EE006EAD1EB99B15006900A4 /* FBElementVisibilityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */; }; + EE05BAFA1D13003C00A3EB00 /* FBKeyboardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE05BAF91D13003C00A3EB00 /* FBKeyboardTests.m */; }; + EE0D1F611EBCDCF7006A3123 /* NSString+FBVisualLength.h in Headers */ = {isa = PBXBuildFile; fileRef = EE0D1F5F1EBCDCF7006A3123 /* NSString+FBVisualLength.h */; }; + EE0D1F621EBCDCF7006A3123 /* NSString+FBVisualLength.m in Sources */ = {isa = PBXBuildFile; fileRef = EE0D1F601EBCDCF7006A3123 /* NSString+FBVisualLength.m */; }; + EE158AAE1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7451CAEDF0C008C271F /* XCUIElement+FBAccessibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AAF1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */; }; + EE158AB01CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AB11CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */; }; + EE158AB21CBD456F00A3E3F0 /* XCUIElement+FBScrolling.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */; }; + EE158AB81CBD456F00A3E3F0 /* FBAlertViewCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */; }; + EE158AB91CBD456F00A3E3F0 /* FBAlertViewCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */; }; + EE158ABA1CBD456F00A3E3F0 /* FBCustomCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */; }; + EE158ABB1CBD456F00A3E3F0 /* FBCustomCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */; }; + EE158ABC1CBD456F00A3E3F0 /* FBDebugCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7541CAEDF0C008C271F /* FBDebugCommands.h */; }; + EE158ABD1CBD456F00A3E3F0 /* FBDebugCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */; }; + EE158ABE1CBD456F00A3E3F0 /* FBElementCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7561CAEDF0C008C271F /* FBElementCommands.h */; }; + EE158ABF1CBD456F00A3E3F0 /* FBElementCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7571CAEDF0C008C271F /* FBElementCommands.m */; }; + EE158AC01CBD456F00A3E3F0 /* FBFindElementCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7581CAEDF0C008C271F /* FBFindElementCommands.h */; }; + EE158AC11CBD456F00A3E3F0 /* FBFindElementCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7591CAEDF0C008C271F /* FBFindElementCommands.m */; }; + EE158AC41CBD456F00A3E3F0 /* FBOrientationCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB75C1CAEDF0C008C271F /* FBOrientationCommands.h */; }; + EE158AC51CBD456F00A3E3F0 /* FBOrientationCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */; }; + EE158AC61CBD456F00A3E3F0 /* FBScreenshotCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB75E1CAEDF0C008C271F /* FBScreenshotCommands.h */; }; + EE158AC71CBD456F00A3E3F0 /* FBScreenshotCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75F1CAEDF0C008C271F /* FBScreenshotCommands.m */; }; + EE158AC81CBD456F00A3E3F0 /* FBSessionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7601CAEDF0C008C271F /* FBSessionCommands.h */; }; + EE158AC91CBD456F00A3E3F0 /* FBSessionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7611CAEDF0C008C271F /* FBSessionCommands.m */; }; + EE158ACA1CBD456F00A3E3F0 /* FBTouchIDCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7621CAEDF0C008C271F /* FBTouchIDCommands.h */; }; + EE158ACB1CBD456F00A3E3F0 /* FBTouchIDCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */; }; + EE158ACC1CBD456F00A3E3F0 /* FBUnknownCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */; }; + EE158ACD1CBD456F00A3E3F0 /* FBUnknownCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */; }; + EE158ACE1CBD456F00A3E3F0 /* FBCommandHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158ACF1CBD456F00A3E3F0 /* FBCommandStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AD01CBD456F00A3E3F0 /* FBElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7791CAEDF0C008C271F /* FBElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AD21CBD456F00A3E3F0 /* FBElementCache.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB77B1CAEDF0C008C271F /* FBElementCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AD31CBD456F00A3E3F0 /* FBElementCache.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC088E41CB56AC000B65968 /* FBElementCache.m */; }; + EE158AD41CBD456F00A3E3F0 /* FBExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EEC088E61CB56DA400B65968 /* FBExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AD51CBD456F00A3E3F0 /* FBExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */; }; + EE158ADA1CBD456F00A3E3F0 /* FBResponseJSONPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7801CAEDF0C008C271F /* FBResponseJSONPayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158ADB1CBD456F00A3E3F0 /* FBResponseJSONPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */; }; + EE158ADC1CBD456F00A3E3F0 /* FBResponsePayload.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7821CAEDF0C008C271F /* FBResponsePayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158ADD1CBD456F00A3E3F0 /* FBResponsePayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7831CAEDF0C008C271F /* FBResponsePayload.m */; }; + EE158ADE1CBD456F00A3E3F0 /* FBRoute.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7841CAEDF0C008C271F /* FBRoute.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158ADF1CBD456F00A3E3F0 /* FBRoute.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7851CAEDF0C008C271F /* FBRoute.m */; }; + EE158AE01CBD456F00A3E3F0 /* FBRouteRequest-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */; }; + EE158AE11CBD456F00A3E3F0 /* FBRouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AE21CBD456F00A3E3F0 /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; }; + EE158AE31CBD456F00A3E3F0 /* FBSession-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */; }; + EE158AE41CBD456F00A3E3F0 /* FBSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78A1CAEDF0C008C271F /* FBSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AE51CBD456F00A3E3F0 /* FBSession.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78B1CAEDF0C008C271F /* FBSession.m */; }; + EE158AE61CBD456F00A3E3F0 /* FBWebServer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78C1CAEDF0C008C271F /* FBWebServer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AE71CBD456F00A3E3F0 /* FBWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */; }; + EE158AE81CBD456F00A3E3F0 /* FBElementTypeTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AE91CBD456F00A3E3F0 /* FBElementTypeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */; }; + EE158AEA1CBD456F00A3E3F0 /* FBRuntimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7911CAEDF0C008C271F /* FBRuntimeUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE158AEB1CBD456F00A3E3F0 /* FBRuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */; }; + EE158B5A1CBD462100A3E3F0 /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; + EE158B5F1CBD47A000A3E3F0 /* WebDriverAgentLib.h in Headers */ = {isa = PBXBuildFile; fileRef = EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE18883A1DA661C400307AA8 /* FBMathUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE1888381DA661C400307AA8 /* FBMathUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE18883B1DA661C400307AA8 /* FBMathUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1888391DA661C400307AA8 /* FBMathUtils.m */; }; + EE18883D1DA663EB00307AA8 /* FBMathUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE18883C1DA663EB00307AA8 /* FBMathUtilsTests.m */; }; + EE1E06DA1D1808C2007CF043 /* FBIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */; }; + EE1E06E71D182E95007CF043 /* FBAlertViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06E61D182E95007CF043 /* FBAlertViewController.m */; }; + EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */; }; + EE2202171ECC612200A29571 /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; + EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE26409A1D0EB5E8009BE6B0 /* FBTapTest.m */; }; + EE26409D1D0EBA25009BE6B0 /* FBElementAttributeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */; }; + EE35AD091E3B77D600A02D78 /* _XCInternalTestRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC981E3B77D600A02D78 /* _XCInternalTestRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0A1E3B77D600A02D78 /* _XCKVOExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC991E3B77D600A02D78 /* _XCKVOExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0B1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9A1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0C1E3B77D600A02D78 /* _XCTestCaseImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9B1E3B77D600A02D78 /* _XCTestCaseImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0D1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9C1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0E1E3B77D600A02D78 /* _XCTestExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9D1E3B77D600A02D78 /* _XCTestExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD0F1E3B77D600A02D78 /* _XCTestImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9E1E3B77D600A02D78 /* _XCTestImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD101E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AC9F1E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD111E3B77D600A02D78 /* _XCTestSuiteImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA01E3B77D600A02D78 /* _XCTestSuiteImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD121E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA11E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD131E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA21E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD141E3B77D600A02D78 /* _XCTWaiterImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA31E3B77D600A02D78 /* _XCTWaiterImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD151E3B77D600A02D78 /* CDStructures.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA41E3B77D600A02D78 /* CDStructures.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD1C1E3B77D600A02D78 /* NSString-XCTAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAB1E3B77D600A02D78 /* NSString-XCTAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD1D1E3B77D600A02D78 /* NSValue-XCTestAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAC1E3B77D600A02D78 /* NSValue-XCTestAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD1E1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAD1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD1F1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAE1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD201E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAF1E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD211E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB01E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD221E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB11E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD231E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB21E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD251E3B77D600A02D78 /* XCActivityRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB41E3B77D600A02D78 /* XCActivityRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD261E3B77D600A02D78 /* XCApplicationMonitor_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB51E3B77D600A02D78 /* XCApplicationMonitor_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD271E3B77D600A02D78 /* XCApplicationMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB61E3B77D600A02D78 /* XCApplicationMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD281E3B77D600A02D78 /* XCApplicationQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD291E3B77D600A02D78 /* XCAXClient_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB81E3B77D600A02D78 /* XCAXClient_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD2A1E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB91E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD2E1E3B77D600A02D78 /* XCEventGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBD1E3B77D600A02D78 /* XCEventGenerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD2F1E3B77D600A02D78 /* XCKeyboardInputSolver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD301E3B77D600A02D78 /* XCKeyboardKeyMap.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBF1E3B77D600A02D78 /* XCKeyboardKeyMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD311E3B77D600A02D78 /* XCKeyboardLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC01E3B77D600A02D78 /* XCKeyboardLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD321E3B77D600A02D78 /* XCKeyMappingPath.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC11E3B77D600A02D78 /* XCKeyMappingPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD331E3B77D600A02D78 /* XCPointerEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC21E3B77D600A02D78 /* XCPointerEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD341E3B77D600A02D78 /* XCPointerEventPath.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC31E3B77D600A02D78 /* XCPointerEventPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD351E3B77D600A02D78 /* XCSourceCodeRecording.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC41E3B77D600A02D78 /* XCSourceCodeRecording.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD361E3B77D600A02D78 /* XCSourceCodeTreeNode.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC51E3B77D600A02D78 /* XCSourceCodeTreeNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD371E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC61E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD381E3B77D600A02D78 /* XCSymbolicationRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC71E3B77D600A02D78 /* XCSymbolicationRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD391E3B77D600A02D78 /* XCSymbolicatorHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC81E3B77D600A02D78 /* XCSymbolicatorHolder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3A1E3B77D600A02D78 /* XCSynthesizedEventRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC91E3B77D600A02D78 /* XCSynthesizedEventRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3B1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCA1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3C1E3B77D600A02D78 /* XCTAsyncActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCB1E3B77D600A02D78 /* XCTAsyncActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3D1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCC1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3E1E3B77D600A02D78 /* XCTAXClient-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCD1E3B77D600A02D78 /* XCTAXClient-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD3F1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCE1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD401E3B77D600A02D78 /* XCTest.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCF1E3B77D600A02D78 /* XCTest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD411E3B77D600A02D78 /* XCTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD01E3B77D600A02D78 /* XCTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD421E3B77D600A02D78 /* XCTestCaseRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD11E3B77D600A02D78 /* XCTestCaseRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD431E3B77D600A02D78 /* XCTestCaseSuite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD21E3B77D600A02D78 /* XCTestCaseSuite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD441E3B77D600A02D78 /* XCTestConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD31E3B77D600A02D78 /* XCTestConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD451E3B77D600A02D78 /* XCTestContext.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD41E3B77D600A02D78 /* XCTestContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD461E3B77D600A02D78 /* XCTestContextScope.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD51E3B77D600A02D78 /* XCTestContextScope.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD471E3B77D600A02D78 /* XCTestDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD61E3B77D600A02D78 /* XCTestDriver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD481E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD71E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD491E3B77D600A02D78 /* XCTestExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD81E3B77D600A02D78 /* XCTestExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4A1E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD91E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4B1E3B77D600A02D78 /* XCTestExpectationWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDA1E3B77D600A02D78 /* XCTestExpectationWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4C1E3B77D600A02D78 /* XCTestLog.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDB1E3B77D600A02D78 /* XCTestLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4D1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDC1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4E1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDD1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD4F1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDE1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD501E3B77D600A02D78 /* XCTestMisuseObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDF1E3B77D600A02D78 /* XCTestMisuseObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD511E3B77D600A02D78 /* XCTestObservation-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE01E3B77D600A02D78 /* XCTestObservation-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD521E3B77D600A02D78 /* XCTestObservationCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE11E3B77D600A02D78 /* XCTestObservationCenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD531E3B77D600A02D78 /* XCTestObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE21E3B77D600A02D78 /* XCTestObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD541E3B77D600A02D78 /* XCTestProbe.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE31E3B77D600A02D78 /* XCTestProbe.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD551E3B77D600A02D78 /* XCTestRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE41E3B77D600A02D78 /* XCTestRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD561E3B77D600A02D78 /* XCTestSuite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE51E3B77D600A02D78 /* XCTestSuite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD571E3B77D600A02D78 /* XCTestSuiteRun.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE61E3B77D600A02D78 /* XCTestSuiteRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD581E3B77D600A02D78 /* XCTestWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE71E3B77D600A02D78 /* XCTestWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD591E3B77D600A02D78 /* XCTKVOExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE81E3B77D600A02D78 /* XCTKVOExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD5A1E3B77D600A02D78 /* XCTMetric.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE91E3B77D600A02D78 /* XCTMetric.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD5B1E3B77D600A02D78 /* XCTNSNotificationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEA1E3B77D600A02D78 /* XCTNSNotificationExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD5C1E3B77D600A02D78 /* XCTNSPredicateExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEB1E3B77D600A02D78 /* XCTNSPredicateExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD5D1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEC1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD5F1E3B77D600A02D78 /* XCTRunnerAutomationSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEE1E3B77D600A02D78 /* XCTRunnerAutomationSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD601E3B77D600A02D78 /* XCTRunnerDaemonSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEF1E3B77D600A02D78 /* XCTRunnerDaemonSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD611E3B77D600A02D78 /* XCTRunnerIDESession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF01E3B77D600A02D78 /* XCTRunnerIDESession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD621E3B77D600A02D78 /* XCTTestRunSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF11E3B77D600A02D78 /* XCTTestRunSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD631E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF21E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD641E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF31E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD651E3B77D600A02D78 /* XCTWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF41E3B77D600A02D78 /* XCTWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD661E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF51E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD671E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF61E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD681E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF71E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD691E3B77D600A02D78 /* XCTWaiterManager.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF81E3B77D600A02D78 /* XCTWaiterManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6A1E3B77D600A02D78 /* XCUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF91E3B77D600A02D78 /* XCUIApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6B1E3B77D600A02D78 /* XCUIApplicationImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFA1E3B77D600A02D78 /* XCUIApplicationImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6C1E3B77D600A02D78 /* XCUIApplicationProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFB1E3B77D600A02D78 /* XCUIApplicationProcess.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6D1E3B77D600A02D78 /* XCUICoordinate.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFC1E3B77D600A02D78 /* XCUICoordinate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6E1E3B77D600A02D78 /* XCUIDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD6F1E3B77D600A02D78 /* XCUIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD701E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFF1E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD721E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD011E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD731E3B77D600A02D78 /* XCUIElementQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD021E3B77D600A02D78 /* XCUIElementQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD751E3B77D600A02D78 /* XCUIRecorderNodeFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD041E3B77D600A02D78 /* XCUIRecorderNodeFinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD761E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD051E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD771E3B77D600A02D78 /* XCUIRecorderTimingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD061E3B77D600A02D78 /* XCUIRecorderTimingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD781E3B77D600A02D78 /* XCUIRecorderUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD071E3B77D600A02D78 /* XCUIRecorderUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE35AD7B1E3B80C000A02D78 /* FBXCTestDaemonsProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD791E3B80C000A02D78 /* FBXCTestDaemonsProxy.h */; }; + EE35AD7C1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */; }; + EE3A18621CDE618F00DE4205 /* FBErrorBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE3A18631CDE618F00DE4205 /* FBErrorBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */; }; + EE3A18661CDE734B00DE4205 /* FBKeyboard.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18641CDE734B00DE4205 /* FBKeyboard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE3A18671CDE734B00DE4205 /* FBKeyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3A18651CDE734B00DE4205 /* FBKeyboard.m */; }; + EE3F8CFE1D08AA17006F02CE /* FBRunLoopSpinnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3F8CFD1D08AA17006F02CE /* FBRunLoopSpinnerTests.m */; }; + EE3F8D001D08B05F006F02CE /* FBElementTypeTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3F8CFF1D08B05F006F02CE /* FBElementTypeTransformerTests.m */; }; + EE5095E51EBCC9090028E2FE /* FBTypingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = AD76723F1D6B826F00610457 /* FBTypingTest.m */; }; + EE5095EB1EBCC9090028E2FE /* XCElementSnapshotHitPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE006EB21EBA1C7B006900A4 /* XCElementSnapshotHitPointTests.m */; }; + EE5095EC1EBCC9090028E2FE /* XCUIApplicationHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06E31D18213F007CF043 /* XCUIApplicationHelperTests.m */; }; + EE5095ED1EBCC9090028E2FE /* XCElementSnapshotHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBDB9A1D1032F0000738CD /* XCElementSnapshotHelperTests.m */; }; + EE5095EE1EBCC9090028E2FE /* FBXPathIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */; }; + EE5095EF1EBCC9090028E2FE /* XCUIElementHelperIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06E11D181CC9007CF043 /* XCUIElementHelperIntegrationTests.m */; }; + EE5095F01EBCC9090028E2FE /* XCUIDeviceHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06DF1D181BB4007CF043 /* XCUIDeviceHelperTests.m */; }; + EE5095F11EBCC9090028E2FE /* XCUIElementFBFindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBD48D1D4785FC00656A81 /* XCUIElementFBFindTests.m */; }; + EE5095F21EBCC9090028E2FE /* XCUIDeviceRotationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 44757A831D42CE8300ECF35E /* XCUIDeviceRotationTests.m */; }; + EE5095F41EBCC9090028E2FE /* XCUIDeviceHealthCheckTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1231D9C08C700E6FFE5 /* XCUIDeviceHealthCheckTests.m */; }; + EE5095F51EBCC9090028E2FE /* XCUIElementAttributesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E504941DF59BAD0020C32A /* XCUIElementAttributesTests.m */; }; + EE5095F91EBCC9090028E2FE /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; + EE5096021EBCD0250028E2FE /* FBIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */; }; + EE55B3251D1D5388003AAAEC /* FBTableDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = EE55B3231D1D5388003AAAEC /* FBTableDataSource.m */; }; + EE55B3271D1D54CF003AAAEC /* FBScrollingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE55B3261D1D54CF003AAAEC /* FBScrollingTests.m */; }; + EE5A24421F136D360078B1D9 /* FBXCodeCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */; }; + EE6A89261D0B19E60083E92B /* FBSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89251D0B19E60083E92B /* FBSessionTests.m */; }; + EE6A892B1D0B25820083E92B /* XCUIApplicationDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */; }; + EE6A892D1D0B2AF40083E92B /* FBErrorBuilderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A892C1D0B2AF40083E92B /* FBErrorBuilderTests.m */; }; + EE6A89371D0B35920083E92B /* FBFailureProofTestCaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */; }; + EE6A893A1D0B38640083E92B /* FBFailureProofTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE6A893B1D0B38640083E92B /* FBFailureProofTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */; }; + EE6B64FD1D0F86EF00E85F5D /* XCTestPrivateSymbols.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */; }; + EE6B64FE1D0F86EF00E85F5D /* XCTestPrivateSymbols.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */; }; + EE7E271C1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE7E271D1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */; }; + EE8BA97A1DCCED9A00A9DEF8 /* FBNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8BA9791DCCED9A00A9DEF8 /* FBNavigationController.m */; }; + EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */; }; + EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE9AB8011CAEE048008C271F /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; }; + EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76571CF7987300275851 /* FBRouteTests.m */; }; + EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76831CF7997600275851 /* AppDelegate.m */; }; + EE9B768F1CF7997600275851 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76851CF7997600275851 /* ViewController.m */; }; + EE9B76911CF7997600275851 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76871CF7997600275851 /* main.m */; }; + EE9B76941CF7997600275851 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EE9B768C1CF7997600275851 /* Main.storyboard */; }; + EE9B769A1CF799F400275851 /* FBAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76991CF799F400275851 /* FBAlertTests.m */; }; + EE9B76A01CF79C0F00275851 /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; + EE9B76A61CF7A43900275851 /* FBConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A11CF7A43900275851 /* FBConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE9B76A71CF7A43900275851 /* FBConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A21CF7A43900275851 /* FBConfiguration.m */; }; + EE9B76A81CF7A43900275851 /* FBLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A31CF7A43900275851 /* FBLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE9B76A91CF7A43900275851 /* FBLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A41CF7A43900275851 /* FBLogger.m */; }; + EE9B76AA1CF7A43900275851 /* FBMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A51CF7A43900275851 /* FBMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEBBD48B1D47746D00656A81 /* XCUIElement+FBFind.h in Headers */ = {isa = PBXBuildFile; fileRef = EEBBD4891D47746D00656A81 /* XCUIElement+FBFind.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEBBD48C1D47746D00656A81 /* XCUIElement+FBFind.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */; }; + EEDFE1211D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEDFE1221D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */; }; + EEE16E971D33A25500172525 /* FBConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE16E961D33A25500172525 /* FBConfigurationTests.m */; }; + EEE376431D59F81400ED88DD /* XCUIDevice+FBRotation.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEE376441D59F81400ED88DD /* XCUIDevice+FBRotation.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE3763E1D59F81400ED88DD /* XCUIDevice+FBRotation.m */; }; + EEE376451D59F81400ED88DD /* XCUIElement+FBUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEE376461D59F81400ED88DD /* XCUIElement+FBUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */; }; + EEE376491D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; }; + EEE9B4721CD02B88009D2030 /* FBRunLoopSpinner.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EEE9B4731CD02B88009D2030 /* FBRunLoopSpinner.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 641EE6FA2240C5F400173FCB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 641EE5D52240C5CA00173FCB; + remoteInfo = WebDriverAgentLib_tvOS; + }; + 64B264FF228C50E0002A5025 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 641EE5D52240C5CA00173FCB; + remoteInfo = WebDriverAgentLib_tvOS; + }; + AD8D96F01D3C12960061268E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE158A981CBD452B00A3E3F0; + remoteInfo = WebDriverAgentLib; + }; + EE158B5B1CBD462500A3E3F0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE158A981CBD452B00A3E3F0; + remoteInfo = WebDriverAgentLib; + }; + EE2202051ECC612200A29571 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE158A981CBD452B00A3E3F0; + remoteInfo = WebDriverAgentLib; + }; + EE2202071ECC612200A29571 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE9B75D31CF7956C00275851; + remoteInfo = Eval; + }; + EE5095DF1EBCC9090028E2FE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE158A981CBD452B00A3E3F0; + remoteInfo = WebDriverAgentLib; + }; + EE5095E11EBCC9090028E2FE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE9B75D31CF7956C00275851; + remoteInfo = Eval; + }; + EE9B75ED1CF7956C00275851 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE9B75D31CF7956C00275851; + remoteInfo = Eval; + }; + EE9B769E1CF79C0A00275851 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE158A981CBD452B00A3E3F0; + remoteInfo = WebDriverAgentLib; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 641EE3472240C1EF00173FCB /* Copy frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 641EE6FD2240C61D00173FCB /* WebDriverAgentLib_tvOS.framework in Copy frameworks */, + ); + name = "Copy frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + EE93CFF41CCA501300708122 /* Copy frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + AD35D06C1CF1C35500870A75 /* WebDriverAgentLib.framework in Copy frameworks */, + ); + name = "Copy frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBMinMax.m"; sourceTree = ""; }; + 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBMinMax.h"; sourceTree = ""; }; + 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIHitPointResult.h; sourceTree = ""; }; + 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBActiveAppDetectionPoint.h; sourceTree = ""; }; + 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBActiveAppDetectionPoint.m; sourceTree = ""; }; + 13DE7A41287C2A8D003243C6 /* FBXCAccessibilityElement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCAccessibilityElement.h; sourceTree = ""; }; + 13DE7A42287C2A8D003243C6 /* FBXCAccessibilityElement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCAccessibilityElement.m; sourceTree = ""; }; + 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCDeviceEvent.h; sourceTree = ""; }; + 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCDeviceEvent.m; sourceTree = ""; }; + 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCElementSnapshot.h; sourceTree = ""; }; + 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCElementSnapshot.m; sourceTree = ""; }; + 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCElementSnapshotWrapper.h; sourceTree = ""; }; + 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCElementSnapshotWrapper.m; sourceTree = ""; }; + 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FBXCElementSnapshotWrapper+Helpers.h"; sourceTree = ""; }; + 13DE7A5A287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FBXCElementSnapshotWrapper+Helpers.m"; sourceTree = ""; }; + 13FFF2F0287DBEE600E561E4 /* XCElementSnapshotDouble.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCElementSnapshotDouble.h; sourceTree = ""; }; + 13FFF2F1287DBEE600E561E4 /* XCElementSnapshotDouble.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCElementSnapshotDouble.m; sourceTree = ""; }; + 1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCTElementSetTransformer-Protocol.h"; sourceTree = ""; }; + 315A14FF2518CB8700A3A064 /* TouchableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchableView.h; sourceTree = ""; }; + 315A15002518CB8700A3A064 /* TouchableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchableView.m; sourceTree = ""; }; + 315A15052518CC2800A3A064 /* TouchSpotView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchSpotView.h; sourceTree = ""; }; + 315A15062518CC2800A3A064 /* TouchSpotView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchSpotView.m; sourceTree = ""; }; + 315A15082518D6F400A3A064 /* TouchViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchViewController.h; sourceTree = ""; }; + 315A15092518D6F400A3A064 /* TouchViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchViewController.m; sourceTree = ""; }; + 44757A831D42CE8300ECF35E /* XCUIDeviceRotationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIDeviceRotationTests.m; sourceTree = ""; }; + 631B523421F6174300625362 /* FBImageProcessorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageProcessorTests.m; sourceTree = ""; }; + 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationProcessDelay.h; sourceTree = ""; }; + 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCUIApplicationProcessDelay.m; sourceTree = ""; }; + 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageProcessor.h; sourceTree = ""; }; + 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageProcessor.m; sourceTree = ""; }; + 641EE2DA2240BBE300173FCB /* WebDriverAgentRunner_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebDriverAgentRunner_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WebDriverAgentLib_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBTVFocuse.h"; sourceTree = ""; }; + 641EE7072240CDEB00173FCB /* XCUIElement+FBTVFocuse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBTVFocuse.m"; sourceTree = ""; }; + 641EE70A2240CE2D00173FCB /* FBTVNavigationTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBTVNavigationTracker.h; sourceTree = ""; }; + 641EE70D2240CE4800173FCB /* FBTVNavigationTracker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBTVNavigationTracker.m; sourceTree = ""; }; + 644D9CCD230E1F1A00C90459 /* FBConfigurationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBConfigurationTests.m; sourceTree = ""; }; + 648C10AA22AAAD9C00B81B9A /* UIKeyboardImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIKeyboardImpl.h; sourceTree = ""; }; + 648C10AE22AAAE4000B81B9A /* TIPreferencesController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TIPreferencesController.h; sourceTree = ""; }; + 6496A5D8230D6EB30087F8CB /* AXSettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AXSettings.h; sourceTree = ""; }; + 64B264EB228C4D54002A5025 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 64B264F3228C5098002A5025 /* FBTVNavigationTrackerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBTVNavigationTrackerTests.m; sourceTree = ""; }; + 64B264F9228C50E0002A5025 /* UnitTests_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 64B26506228C54F2002A5025 /* XCUIElementDouble.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIElementDouble.h; sourceTree = ""; }; + 64B26507228C5514002A5025 /* XCUIElementDouble.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCUIElementDouble.m; sourceTree = ""; }; + 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FBTVNavigationTracker-Private.h"; sourceTree = ""; }; + 711084421DA3AA7500F913D6 /* FBXPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXPath.h; sourceTree = ""; }; + 711084431DA3AA7500F913D6 /* FBXPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPath.m; sourceTree = ""; }; + 7119097B2152580600BA3C7E /* XCUIScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIScreen.h; sourceTree = ""; }; + 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBPickerWheelSelectTests.m; sourceTree = ""; }; + 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIScreenDataSource-Protocol.h"; sourceTree = ""; }; + 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBTouchActionCommands.h; sourceTree = ""; }; + 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBTouchActionCommands.m; sourceTree = ""; }; + 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CTouchActionsIntegrationTests.m; sourceTree = ""; }; + 71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CMultiTouchActionsIntegrationTests.m; sourceTree = ""; }; + 712A0C841DA3E459007D02E5 /* FBXPathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPathTests.m; sourceTree = ""; }; + 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBXPath-Private.h"; sourceTree = ""; }; + 713352FC26CEF31D00523CBC /* FBLRUCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBLRUCacheTests.m; sourceTree = ""; }; + 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBPickerWheel.h"; sourceTree = ""; }; + 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBPickerWheel.m"; sourceTree = ""; }; + 7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CTypeActionsTests.m; sourceTree = ""; }; + 713914591DF01989005896C2 /* XCUIElementHelpersTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIElementHelpersTests.m; sourceTree = ""; }; + 7139145B1DF01A12005896C2 /* NSExpressionFBFormatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSExpressionFBFormatTests.m; sourceTree = ""; }; + 713AE573243A53BE0000D657 /* FBW3CActionsHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBW3CActionsHelpers.h; sourceTree = ""; }; + 713AE574243A53BE0000D657 /* FBW3CActionsHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CActionsHelpers.m; sourceTree = ""; }; + 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElementUtils.h; sourceTree = ""; }; + 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementUtils.m; sourceTree = ""; }; + 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBBaseActionsSynthesizer.h; sourceTree = ""; }; + 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBW3CActionsSynthesizer.h; sourceTree = ""; }; + 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CActionsSynthesizer.m; sourceTree = ""; }; + 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBBaseActionsSynthesizer.m; sourceTree = ""; }; + 71414ED02670A1ED003A8C5D /* LRUCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LRUCache.h; sourceTree = ""; }; + 71414ED12670A1ED003A8C5D /* LRUCacheNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LRUCacheNode.h; sourceTree = ""; }; + 71414ED22670A1ED003A8C5D /* LRUCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LRUCache.m; sourceTree = ""; }; + 71414ED32670A1ED003A8C5D /* LRUCacheNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LRUCacheNode.m; sourceTree = ""; }; + 714801D01FA9D9FA00DC5997 /* FBSDKVersionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSDKVersionTests.m; sourceTree = ""; }; + 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPathIntegrationTests.m; sourceTree = ""; }; + 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXMLGenerationOptions.h; sourceTree = ""; }; + 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXMLGenerationOptions.m; sourceTree = ""; }; + 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCAXClient_iOS+FBSnapshotReqParams.h"; sourceTree = ""; }; + 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCAXClient_iOS+FBSnapshotReqParams.m"; sourceTree = ""; }; + 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBCapabilities.h; sourceTree = ""; }; + 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCapabilities.m; sourceTree = ""; }; + 7150348521A6DAD600A0F4BA /* FBImageUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageUtils.h; sourceTree = ""; }; + 7150348621A6DAD600A0F4BA /* FBImageUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageUtils.m; sourceTree = ""; }; + 7152EB2F1F41F9960047EEFF /* FBSessionIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSessionIntegrationTests.m; sourceTree = ""; }; + 715557D1211DBCE700613B26 /* FBTCPSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBTCPSocket.h; sourceTree = ""; }; + 715557D2211DBCE700613B26 /* FBTCPSocket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBTCPSocket.m; sourceTree = ""; }; + 71555A3B1DEC460A007D4A8B /* NSExpression+FBFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSExpression+FBFormat.h"; sourceTree = ""; }; + 71555A3C1DEC460A007D4A8B /* NSExpression+FBFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSExpression+FBFormat.m"; sourceTree = ""; }; + 7155B419224D5B460042A993 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.2.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; }; + 7155B41A224D5B480042A993 /* libAccessibility.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAccessibility.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.2.sdk/usr/lib/libAccessibility.tbd; sourceTree = DEVELOPER_DIR; }; + 7155B423224D5B980042A993 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 7155B425224D5C130042A993 /* XCTAutomationSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTAutomationSupport.framework; path = Platforms/AppleTVOS.platform/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework; sourceTree = DEVELOPER_DIR; }; + 7155D701211DCEF400166C20 /* FBMjpegServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBMjpegServer.h; sourceTree = ""; }; + 7155D702211DCEF400166C20 /* FBMjpegServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBMjpegServer.m; sourceTree = ""; }; + 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCAXClientProxy.h; sourceTree = ""; }; + 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCAXClientProxy.m; sourceTree = ""; }; + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBElementHelpers.h; sourceTree = ""; }; + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementHelpers.m; sourceTree = ""; }; + 715AFABF1FFA29180053896D /* FBScreen.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreen.h; sourceTree = ""; }; + 715AFAC01FFA29180053896D /* FBScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreen.m; sourceTree = ""; }; + 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenTests.m; sourceTree = ""; }; + 715D554A2229891B00524509 /* FBExceptionHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptionHandlerTests.m; sourceTree = ""; }; + 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IOSTestSettings.xcconfig; sourceTree = ""; }; + 716C9342224D53A1004B8542 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/AppleTVOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 716C9343224D53DF004B8542 /* libAccessibility.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAccessibility.tbd; path = usr/lib/libAccessibility.tbd; sourceTree = SDKROOT; }; + 716C9344224D53FC004B8542 /* XCTAutomationSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTAutomationSupport.framework; path = Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework; sourceTree = DEVELOPER_DIR; }; + 716C9345224D540C004B8542 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; + 716C9DF827315D21005AD475 /* FBReflectionUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBReflectionUtils.h; sourceTree = ""; }; + 716C9DF927315D21005AD475 /* FBReflectionUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBReflectionUtils.m; sourceTree = ""; }; + 716C9DFE27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBUIInterruptions.h"; sourceTree = ""; }; + 716C9DFF27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBUIInterruptions.m"; sourceTree = ""; }; + 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FBXMLSafeString.h"; sourceTree = ""; }; + 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FBXMLSafeString.m"; sourceTree = ""; }; + 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXMLSafeStringTests.m; sourceTree = ""; }; + 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+FBUtf8SafeDictionary.h"; sourceTree = ""; }; + 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+FBUtf8SafeDictionary.m"; sourceTree = ""; }; + 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSDictionaryFBUtf8SafeTests.m; sourceTree = ""; }; + 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSSettings.xcconfig; sourceTree = ""; }; + 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSTestSettings.xcconfig; sourceTree = ""; }; + 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncUdpSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h; sourceTree = SOURCE_ROOT; }; + 718226C72587443600661B83 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h; sourceTree = SOURCE_ROOT; }; + 718226C82587443600661B83 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m; sourceTree = SOURCE_ROOT; }; + 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncUdpSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m; sourceTree = SOURCE_ROOT; }; + 718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBProtocolHelpersTests.m; sourceTree = ""; }; + 71930C4020662E1F00D3AFEC /* FBPasteboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBPasteboard.h; sourceTree = ""; }; + 71930C4120662E1F00D3AFEC /* FBPasteboard.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBPasteboard.m; sourceTree = ""; }; + 71930C462066434000D3AFEC /* FBPasteboardTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBPasteboardTests.m; sourceTree = ""; }; + 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAlertsMonitor.h; sourceTree = ""; }; + 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAlertsMonitor.m; sourceTree = ""; }; + 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBAlert.h"; sourceTree = ""; }; + 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBAlert.m"; sourceTree = ""; }; + 719CD8FE2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAutoAlertsHandlerTests.m; sourceTree = ""; }; + 719DCF132601EAFB000E765F /* FBNotificationsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBNotificationsHelper.h; sourceTree = ""; }; + 719DCF142601EAFB000E765F /* FBNotificationsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBNotificationsHelper.m; sourceTree = ""; }; + 719FF5B81DAD21F5008E0099 /* FBElementUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementUtilitiesTests.m; sourceTree = ""; }; + 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSPredicate+FBFormat.h"; path = "../Utilities/NSPredicate+FBFormat.h"; sourceTree = ""; }; + 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSPredicate+FBFormat.m"; path = "../Utilities/NSPredicate+FBFormat.m"; sourceTree = ""; }; + 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSPredicateFBFormatTests.m; sourceTree = ""; }; + 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTIssue+FBPatcher.h"; sourceTree = ""; }; + 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTIssue+FBPatcher.m"; sourceTree = ""; }; + 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBClassChain.h"; sourceTree = ""; }; + 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBClassChain.m"; sourceTree = ""; }; + 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBClassChainQueryParser.h; sourceTree = ""; }; + 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainQueryParser.m; sourceTree = ""; }; + 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainTests.m; sourceTree = ""; }; + 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSafariAlertTests.m; sourceTree = ""; }; + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBVisibleFrame.h"; sourceTree = ""; }; + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBVisibleFrame.m"; sourceTree = ""; }; + 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBHTTPStatusCodes.h; sourceTree = ""; }; + 71B155DB230711E900646AFB /* FBCommandStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCommandStatus.m; sourceTree = ""; }; + 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBProtocolHelpers.h; sourceTree = ""; }; + 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBProtocolHelpers.m; sourceTree = ""; }; + 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBUID.h"; sourceTree = ""; }; + 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBUID.m"; sourceTree = ""; }; + 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBVideoRecordingTests.m; sourceTree = ""; }; + 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingPromise.h; sourceTree = ""; }; + 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingPromise.m; sourceTree = ""; }; + 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingRequest.h; sourceTree = ""; }; + 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingRequest.m; sourceTree = ""; }; + 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBVideoCommands.h; sourceTree = ""; }; + 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBVideoCommands.m; sourceTree = ""; }; + 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingContainer.h; sourceTree = ""; }; + 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingContainer.m; sourceTree = ""; }; + 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBTouchAction.h"; sourceTree = ""; }; + 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBTouchAction.m"; sourceTree = ""; }; + 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBQuiescence.h"; sourceTree = ""; }; + 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBQuiescence.m"; sourceTree = ""; }; + 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenshot.h; sourceTree = ""; }; + 71C9EAAB25E8415A00470CD8 /* FBScreenshot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenshot.m; sourceTree = ""; }; + 71D04DC625356C43008A052C /* XCUIElement+FBCaching.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBCaching.h"; sourceTree = ""; }; + 71D04DC725356C43008A052C /* XCUIElement+FBCaching.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBCaching.m"; sourceTree = ""; }; + 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBResolve.h"; sourceTree = ""; }; + 71D3B3D4267FC7260076473D /* XCUIElement+FBResolve.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBResolve.m"; sourceTree = ""; }; + 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplicationProcess+FBQuiescence.h"; sourceTree = ""; }; + 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplicationProcess+FBQuiescence.m"; sourceTree = ""; }; + 71E504941DF59BAD0020C32A /* XCUIElementAttributesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIElementAttributesTests.m; sourceTree = ""; }; + 71E75E6B254824230099FC87 /* XCUIElementQuery+FBHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElementQuery+FBHelpers.h"; sourceTree = ""; }; + 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElementQuery+FBHelpers.m"; sourceTree = ""; }; + 71F3E7D225417FF400E0C22B /* FBSettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBSettings.h; sourceTree = ""; }; + 71F3E7D325417FF400E0C22B /* FBSettings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSettings.m; sourceTree = ""; }; + 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBSwiping.h"; sourceTree = ""; }; + 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBSwiping.m"; sourceTree = ""; }; + 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementSwipingTests.m; sourceTree = ""; }; + 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBExceptions.h; sourceTree = ""; }; + 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptions.m; sourceTree = ""; }; + AD42DD2A1CF121E600806E5D /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + AD6C26921CF2379700F8B5FF /* FBAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FBAlert.h; path = WebDriverAgentLib/FBAlert.h; sourceTree = SOURCE_ROOT; }; + AD6C26931CF2379700F8B5FF /* FBAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; name = FBAlert.m; path = WebDriverAgentLib/FBAlert.m; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBHelpers.h"; sourceTree = ""; }; + AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBHelpers.m"; sourceTree = ""; }; + AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBHelpers.h"; sourceTree = ""; }; + AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBHelpers.m"; sourceTree = ""; }; + AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBTyping.h"; sourceTree = ""; }; + AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBTyping.m"; sourceTree = ""; }; + AD76723F1D6B826F00610457 /* FBTypingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBTypingTest.m; sourceTree = ""; }; + ADBC39931D0782CD00327304 /* FBElementCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementCacheTests.m; sourceTree = ""; }; + ADBC39961D07842800327304 /* XCUIElementDouble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIElementDouble.h; sourceTree = ""; }; + ADBC39971D07842800327304 /* XCUIElementDouble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIElementDouble.m; sourceTree = ""; }; + ADDA07221D6BB2BF001700AC /* FBScrollViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBScrollViewController.h; sourceTree = ""; }; + ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBScrollViewController.m; sourceTree = ""; }; + ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtilsTests.m; sourceTree = ""; }; + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAccessibilityTraits.m; sourceTree = ""; }; + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAccessibilityTraits.h; sourceTree = ""; }; + C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSApplicationWorkspace.h; sourceTree = ""; }; + C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBUnattachedAppLauncher.h; sourceTree = ""; }; + C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBUnattachedAppLauncher.m; sourceTree = ""; }; + E444DC59249131880060D7EB /* HTTPErrorResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPErrorResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h; sourceTree = SOURCE_ROOT; }; + E444DC5B249131880060D7EB /* HTTPDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPDataResponse.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m; sourceTree = SOURCE_ROOT; }; + E444DC60249131890060D7EB /* HTTPDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPDataResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h; sourceTree = SOURCE_ROOT; }; + E444DC61249131890060D7EB /* HTTPErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPErrorResponse.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m; sourceTree = SOURCE_ROOT; }; + E444DC7B249131B00060D7EB /* DDRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDRange.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h; sourceTree = SOURCE_ROOT; }; + E444DC7D249131B00060D7EB /* DDNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDNumber.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h; sourceTree = SOURCE_ROOT; }; + E444DC7E249131B00060D7EB /* DDRange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDRange.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m; sourceTree = SOURCE_ROOT; }; + E444DC7F249131B00060D7EB /* DDNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDNumber.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m; sourceTree = SOURCE_ROOT; }; + E444DC87249131D30060D7EB /* HTTPMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPMessage.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h; sourceTree = SOURCE_ROOT; }; + E444DC89249131D30060D7EB /* HTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPConnection.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h; sourceTree = SOURCE_ROOT; }; + E444DC8B249131D30060D7EB /* HTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPServer.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h; sourceTree = SOURCE_ROOT; }; + E444DC8C249131D30060D7EB /* HTTPConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPConnection.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m; sourceTree = SOURCE_ROOT; }; + E444DC8D249131D30060D7EB /* HTTPLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPLogging.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h; sourceTree = SOURCE_ROOT; }; + E444DC8F249131D40060D7EB /* HTTPResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h; sourceTree = SOURCE_ROOT; }; + E444DC90249131D40060D7EB /* HTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPServer.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m; sourceTree = SOURCE_ROOT; }; + E444DC91249131D40060D7EB /* HTTPMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPMessage.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m; sourceTree = SOURCE_ROOT; }; + E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPResponseProxy.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m; sourceTree = SOURCE_ROOT; }; + E444DCA024913C210060D7EB /* Route.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Route.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m; sourceTree = SOURCE_ROOT; }; + E444DCA124913C210060D7EB /* RouteResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RouteResponse.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h; sourceTree = SOURCE_ROOT; }; + E444DCA224913C210060D7EB /* HTTPResponseProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPResponseProxy.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h; sourceTree = SOURCE_ROOT; }; + E444DCA324913C210060D7EB /* Route.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Route.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h; sourceTree = SOURCE_ROOT; }; + E444DCA424913C210060D7EB /* RouteResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RouteResponse.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m; sourceTree = SOURCE_ROOT; }; + E444DCA524913C210060D7EB /* RoutingConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RoutingConnection.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h; sourceTree = SOURCE_ROOT; }; + E444DCA624913C210060D7EB /* RoutingConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RoutingConnection.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m; sourceTree = SOURCE_ROOT; }; + E444DCA724913C210060D7EB /* RoutingHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RoutingHTTPServer.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h; sourceTree = SOURCE_ROOT; }; + E444DCA824913C220060D7EB /* RoutingHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RoutingHTTPServer.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m; sourceTree = SOURCE_ROOT; }; + E444DCA924913C220060D7EB /* RouteRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RouteRequest.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m; sourceTree = SOURCE_ROOT; }; + E444DCAA24913C220060D7EB /* RouteRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RouteRequest.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h; sourceTree = SOURCE_ROOT; }; + EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementVisibilityTests.m; sourceTree = ""; }; + EE006EB21EBA1C7B006900A4 /* XCElementSnapshotHitPointTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCElementSnapshotHitPointTests.m; sourceTree = ""; }; + EE05BAF91D13003C00A3EB00 /* FBKeyboardTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBKeyboardTests.m; sourceTree = ""; }; + EE0D1F5F1EBCDCF7006A3123 /* NSString+FBVisualLength.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FBVisualLength.h"; sourceTree = ""; }; + EE0D1F601EBCDCF7006A3123 /* NSString+FBVisualLength.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FBVisualLength.m"; sourceTree = ""; }; + EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WebDriverAgentLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EE158B5D1CBD479000A3E3F0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = WebDriverAgentLib/Info.plist; sourceTree = SOURCE_ROOT; }; + EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WebDriverAgentLib.h; path = WebDriverAgentLib/WebDriverAgentLib.h; sourceTree = SOURCE_ROOT; }; + EE1888381DA661C400307AA8 /* FBMathUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBMathUtils.h; sourceTree = ""; }; + EE1888391DA661C400307AA8 /* FBMathUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBMathUtils.m; sourceTree = ""; }; + EE18883C1DA663EB00307AA8 /* FBMathUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBMathUtilsTests.m; sourceTree = ""; }; + EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBIntegrationTestCase.m; sourceTree = ""; }; + EE1E06DB1D18090F007CF043 /* FBIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBIntegrationTestCase.h; sourceTree = ""; }; + EE1E06DC1D1811C4007CF043 /* FBTestMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBTestMacros.h; sourceTree = ""; }; + EE1E06DF1D181BB4007CF043 /* XCUIDeviceHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIDeviceHelperTests.m; sourceTree = ""; }; + EE1E06E11D181CC9007CF043 /* XCUIElementHelperIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIElementHelperIntegrationTests.m; sourceTree = ""; }; + EE1E06E31D18213F007CF043 /* XCUIApplicationHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIApplicationHelperTests.m; sourceTree = ""; }; + EE1E06E51D182E95007CF043 /* FBAlertViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBAlertViewController.h; sourceTree = ""; }; + EE1E06E61D182E95007CF043 /* FBAlertViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBAlertViewController.m; sourceTree = ""; }; + EE22021C1ECC612200A29571 /* IntegrationTests_3.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_3.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EE26409A1D0EB5E8009BE6B0 /* FBTapTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBTapTest.m; sourceTree = ""; }; + EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementAttributeTests.m; sourceTree = ""; }; + EE35AC981E3B77D600A02D78 /* _XCInternalTestRun.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCInternalTestRun.h; sourceTree = ""; }; + EE35AC991E3B77D600A02D78 /* _XCKVOExpectationImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCKVOExpectationImplementation.h; sourceTree = ""; }; + EE35AC9A1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTDarwinNotificationExpectationImplementation.h; sourceTree = ""; }; + EE35AC9B1E3B77D600A02D78 /* _XCTestCaseImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestCaseImplementation.h; sourceTree = ""; }; + EE35AC9C1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestCaseInterruptionException.h; sourceTree = ""; }; + EE35AC9D1E3B77D600A02D78 /* _XCTestExpectationImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestExpectationImplementation.h; sourceTree = ""; }; + EE35AC9E1E3B77D600A02D78 /* _XCTestImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestImplementation.h; sourceTree = ""; }; + EE35AC9F1E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestObservationCenterImplementation.h; sourceTree = ""; }; + EE35ACA01E3B77D600A02D78 /* _XCTestSuiteImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTestSuiteImplementation.h; sourceTree = ""; }; + EE35ACA11E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTNSNotificationExpectationImplementation.h; sourceTree = ""; }; + EE35ACA21E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTNSPredicateExpectationImplementation.h; sourceTree = ""; }; + EE35ACA31E3B77D600A02D78 /* _XCTWaiterImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _XCTWaiterImpl.h; sourceTree = ""; }; + EE35ACA41E3B77D600A02D78 /* CDStructures.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDStructures.h; sourceTree = ""; }; + EE35ACAB1E3B77D600A02D78 /* NSString-XCTAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString-XCTAdditions.h"; sourceTree = ""; }; + EE35ACAC1E3B77D600A02D78 /* NSValue-XCTestAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSValue-XCTestAdditions.h"; sourceTree = ""; }; + EE35ACAD1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACAE1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILongPressGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACAF1E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPanGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACB01E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPinchGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACB11E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UISwipeGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACB21E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITapGestureRecognizer-RecordingAdditions.h"; sourceTree = ""; }; + EE35ACB41E3B77D600A02D78 /* XCActivityRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCActivityRecord.h; sourceTree = ""; }; + EE35ACB51E3B77D600A02D78 /* XCApplicationMonitor_iOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCApplicationMonitor_iOS.h; sourceTree = ""; }; + EE35ACB61E3B77D600A02D78 /* XCApplicationMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCApplicationMonitor.h; sourceTree = ""; }; + EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCApplicationQuery.h; sourceTree = ""; }; + EE35ACB81E3B77D600A02D78 /* XCAXClient_iOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCAXClient_iOS.h; sourceTree = ""; }; + EE35ACB91E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCDebugLogDelegate-Protocol.h"; sourceTree = ""; }; + EE35ACBD1E3B77D600A02D78 /* XCEventGenerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCEventGenerator.h; sourceTree = ""; }; + EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCKeyboardInputSolver.h; sourceTree = ""; }; + EE35ACBF1E3B77D600A02D78 /* XCKeyboardKeyMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCKeyboardKeyMap.h; sourceTree = ""; }; + EE35ACC01E3B77D600A02D78 /* XCKeyboardLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCKeyboardLayout.h; sourceTree = ""; }; + EE35ACC11E3B77D600A02D78 /* XCKeyMappingPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCKeyMappingPath.h; sourceTree = ""; }; + EE35ACC21E3B77D600A02D78 /* XCPointerEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCPointerEvent.h; sourceTree = ""; }; + EE35ACC31E3B77D600A02D78 /* XCPointerEventPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCPointerEventPath.h; sourceTree = ""; }; + EE35ACC41E3B77D600A02D78 /* XCSourceCodeRecording.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSourceCodeRecording.h; sourceTree = ""; }; + EE35ACC51E3B77D600A02D78 /* XCSourceCodeTreeNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSourceCodeTreeNode.h; sourceTree = ""; }; + EE35ACC61E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSourceCodeTreeNodeEnumerator.h; sourceTree = ""; }; + EE35ACC71E3B77D600A02D78 /* XCSymbolicationRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSymbolicationRecord.h; sourceTree = ""; }; + EE35ACC81E3B77D600A02D78 /* XCSymbolicatorHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSymbolicatorHolder.h; sourceTree = ""; }; + EE35ACC91E3B77D600A02D78 /* XCSynthesizedEventRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCSynthesizedEventRecord.h; sourceTree = ""; }; + EE35ACCA1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTAsyncActivity-Protocol.h"; sourceTree = ""; }; + EE35ACCB1E3B77D600A02D78 /* XCTAsyncActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTAsyncActivity.h; sourceTree = ""; }; + EE35ACCC1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTAutomationTarget-Protocol.h"; sourceTree = ""; }; + EE35ACCD1E3B77D600A02D78 /* XCTAXClient-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTAXClient-Protocol.h"; sourceTree = ""; }; + EE35ACCE1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTDarwinNotificationExpectation.h; sourceTree = ""; }; + EE35ACCF1E3B77D600A02D78 /* XCTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTest.h; sourceTree = ""; }; + EE35ACD01E3B77D600A02D78 /* XCTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestCase.h; sourceTree = ""; }; + EE35ACD11E3B77D600A02D78 /* XCTestCaseRun.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestCaseRun.h; sourceTree = ""; }; + EE35ACD21E3B77D600A02D78 /* XCTestCaseSuite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestCaseSuite.h; sourceTree = ""; }; + EE35ACD31E3B77D600A02D78 /* XCTestConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestConfiguration.h; sourceTree = ""; }; + EE35ACD41E3B77D600A02D78 /* XCTestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestContext.h; sourceTree = ""; }; + EE35ACD51E3B77D600A02D78 /* XCTestContextScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestContextScope.h; sourceTree = ""; }; + EE35ACD61E3B77D600A02D78 /* XCTestDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestDriver.h; sourceTree = ""; }; + EE35ACD71E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestDriverInterface-Protocol.h"; sourceTree = ""; }; + EE35ACD81E3B77D600A02D78 /* XCTestExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestExpectation.h; sourceTree = ""; }; + EE35ACD91E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestExpectationDelegate-Protocol.h"; sourceTree = ""; }; + EE35ACDA1E3B77D600A02D78 /* XCTestExpectationWaiter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestExpectationWaiter.h; sourceTree = ""; }; + EE35ACDB1E3B77D600A02D78 /* XCTestLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestLog.h; sourceTree = ""; }; + EE35ACDC1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestManager_IDEInterface-Protocol.h"; sourceTree = ""; }; + EE35ACDD1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestManager_ManagerInterface-Protocol.h"; sourceTree = ""; }; + EE35ACDE1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestManager_TestsInterface-Protocol.h"; sourceTree = ""; }; + EE35ACDF1E3B77D600A02D78 /* XCTestMisuseObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestMisuseObserver.h; sourceTree = ""; }; + EE35ACE01E3B77D600A02D78 /* XCTestObservation-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestObservation-Protocol.h"; sourceTree = ""; }; + EE35ACE11E3B77D600A02D78 /* XCTestObservationCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestObservationCenter.h; sourceTree = ""; }; + EE35ACE21E3B77D600A02D78 /* XCTestObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestObserver.h; sourceTree = ""; }; + EE35ACE31E3B77D600A02D78 /* XCTestProbe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestProbe.h; sourceTree = ""; }; + EE35ACE41E3B77D600A02D78 /* XCTestRun.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestRun.h; sourceTree = ""; }; + EE35ACE51E3B77D600A02D78 /* XCTestSuite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestSuite.h; sourceTree = ""; }; + EE35ACE61E3B77D600A02D78 /* XCTestSuiteRun.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestSuiteRun.h; sourceTree = ""; }; + EE35ACE71E3B77D600A02D78 /* XCTestWaiter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestWaiter.h; sourceTree = ""; }; + EE35ACE81E3B77D600A02D78 /* XCTKVOExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTKVOExpectation.h; sourceTree = ""; }; + EE35ACE91E3B77D600A02D78 /* XCTMetric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTMetric.h; sourceTree = ""; }; + EE35ACEA1E3B77D600A02D78 /* XCTNSNotificationExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTNSNotificationExpectation.h; sourceTree = ""; }; + EE35ACEB1E3B77D600A02D78 /* XCTNSPredicateExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTNSPredicateExpectation.h; sourceTree = ""; }; + EE35ACEC1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTNSPredicateExpectationObject-Protocol.h"; sourceTree = ""; }; + EE35ACEE1E3B77D600A02D78 /* XCTRunnerAutomationSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTRunnerAutomationSession.h; sourceTree = ""; }; + EE35ACEF1E3B77D600A02D78 /* XCTRunnerDaemonSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTRunnerDaemonSession.h; sourceTree = ""; }; + EE35ACF01E3B77D600A02D78 /* XCTRunnerIDESession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTRunnerIDESession.h; sourceTree = ""; }; + EE35ACF11E3B77D600A02D78 /* XCTTestRunSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTTestRunSession.h; sourceTree = ""; }; + EE35ACF21E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTTestRunSessionDelegate-Protocol.h"; sourceTree = ""; }; + EE35ACF31E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTUIApplicationMonitor-Protocol.h"; sourceTree = ""; }; + EE35ACF41E3B77D600A02D78 /* XCTWaiter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTWaiter.h; sourceTree = ""; }; + EE35ACF51E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTWaiterDelegate-Protocol.h"; sourceTree = ""; }; + EE35ACF61E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTWaiterDelegatePrivate-Protocol.h"; sourceTree = ""; }; + EE35ACF71E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTWaiterManagement-Protocol.h"; sourceTree = ""; }; + EE35ACF81E3B77D600A02D78 /* XCTWaiterManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTWaiterManager.h; sourceTree = ""; }; + EE35ACF91E3B77D600A02D78 /* XCUIApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIApplication.h; sourceTree = ""; }; + EE35ACFA1E3B77D600A02D78 /* XCUIApplicationImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationImpl.h; sourceTree = ""; }; + EE35ACFB1E3B77D600A02D78 /* XCUIApplicationProcess.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationProcess.h; sourceTree = ""; }; + EE35ACFC1E3B77D600A02D78 /* XCUICoordinate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUICoordinate.h; sourceTree = ""; }; + EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIDevice.h; sourceTree = ""; }; + EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIElement.h; sourceTree = ""; }; + EE35ACFF1E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIElementAsynchronousHandlerWrapper.h; sourceTree = ""; }; + EE35AD011E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIElementHitPointCoordinate.h; sourceTree = ""; }; + EE35AD021E3B77D600A02D78 /* XCUIElementQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIElementQuery.h; sourceTree = ""; }; + EE35AD041E3B77D600A02D78 /* XCUIRecorderNodeFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIRecorderNodeFinder.h; sourceTree = ""; }; + EE35AD051E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIRecorderNodeFinderMatch.h; sourceTree = ""; }; + EE35AD061E3B77D600A02D78 /* XCUIRecorderTimingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIRecorderTimingMessage.h; sourceTree = ""; }; + EE35AD071E3B77D600A02D78 /* XCUIRecorderUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIRecorderUtilities.h; sourceTree = ""; }; + EE35AD791E3B80C000A02D78 /* FBXCTestDaemonsProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXCTestDaemonsProxy.h; sourceTree = ""; }; + EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXCTestDaemonsProxy.m; sourceTree = ""; }; + EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBErrorBuilder.h; sourceTree = ""; }; + EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBErrorBuilder.m; sourceTree = ""; }; + EE3A18641CDE734B00DE4205 /* FBKeyboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FBKeyboard.h; path = WebDriverAgentLib/Utilities/FBKeyboard.h; sourceTree = SOURCE_ROOT; }; + EE3A18651CDE734B00DE4205 /* FBKeyboard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FBKeyboard.m; path = WebDriverAgentLib/Utilities/FBKeyboard.m; sourceTree = SOURCE_ROOT; }; + EE3F8CFD1D08AA17006F02CE /* FBRunLoopSpinnerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRunLoopSpinnerTests.m; sourceTree = ""; }; + EE3F8CFF1D08B05F006F02CE /* FBElementTypeTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementTypeTransformerTests.m; sourceTree = ""; }; + EE5095FE1EBCC9090028E2FE /* IntegrationTests_2.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_2.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EE55B3221D1D5388003AAAEC /* FBTableDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBTableDataSource.h; sourceTree = ""; }; + EE55B3231D1D5388003AAAEC /* FBTableDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBTableDataSource.m; sourceTree = ""; }; + EE55B3261D1D54CF003AAAEC /* FBScrollingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBScrollingTests.m; sourceTree = ""; }; + EE5A24401F136C8D0078B1D9 /* FBXCodeCompatibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCodeCompatibility.h; sourceTree = ""; }; + EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCodeCompatibility.m; sourceTree = ""; }; + EE6A89251D0B19E60083E92B /* FBSessionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSessionTests.m; sourceTree = ""; }; + EE6A89271D0B257B0083E92B /* XCUIApplicationDouble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationDouble.h; sourceTree = ""; }; + EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIApplicationDouble.m; sourceTree = ""; }; + EE6A892C1D0B2AF40083E92B /* FBErrorBuilderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBErrorBuilderTests.m; sourceTree = ""; }; + EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBFailureProofTestCaseTests.m; sourceTree = ""; }; + EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBFailureProofTestCase.h; sourceTree = ""; }; + EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBFailureProofTestCase.m; sourceTree = ""; }; + EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCTestPrivateSymbols.h; sourceTree = ""; }; + EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCTestPrivateSymbols.m; sourceTree = ""; }; + EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBDebugLogDelegateDecorator.h; sourceTree = ""; }; + EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBDebugLogDelegateDecorator.m; sourceTree = ""; }; + EE836C021C0F118600D87246 /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EE8BA9781DCCED9A00A9DEF8 /* FBNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBNavigationController.h; sourceTree = ""; }; + EE8BA9791DCCED9A00A9DEF8 /* FBNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBNavigationController.m; sourceTree = ""; }; + EE8DDD7A20C57320004D4925 /* FBForceTouchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBForceTouchTests.m; sourceTree = ""; }; + EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBForceTouch.m"; sourceTree = ""; }; + EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBForceTouch.h"; sourceTree = ""; }; + EE9AB7451CAEDF0C008C271F /* XCUIElement+FBAccessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBAccessibility.h"; sourceTree = ""; }; + EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBAccessibility.m"; sourceTree = ""; }; + EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBIsVisible.h"; sourceTree = ""; }; + EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBIsVisible.m"; sourceTree = ""; }; + EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBScrolling.h"; sourceTree = ""; }; + EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBScrolling.m"; sourceTree = ""; }; + EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBAlertViewCommands.h; sourceTree = ""; }; + EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBAlertViewCommands.m; sourceTree = ""; }; + EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCustomCommands.h; sourceTree = ""; }; + EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBCustomCommands.m; sourceTree = ""; }; + EE9AB7541CAEDF0C008C271F /* FBDebugCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBDebugCommands.h; sourceTree = ""; }; + EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBDebugCommands.m; sourceTree = ""; }; + EE9AB7561CAEDF0C008C271F /* FBElementCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElementCommands.h; sourceTree = ""; }; + EE9AB7571CAEDF0C008C271F /* FBElementCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementCommands.m; sourceTree = ""; }; + EE9AB7581CAEDF0C008C271F /* FBFindElementCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBFindElementCommands.h; sourceTree = ""; }; + EE9AB7591CAEDF0C008C271F /* FBFindElementCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBFindElementCommands.m; sourceTree = ""; }; + EE9AB75C1CAEDF0C008C271F /* FBOrientationCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBOrientationCommands.h; sourceTree = ""; }; + EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBOrientationCommands.m; sourceTree = ""; }; + EE9AB75E1CAEDF0C008C271F /* FBScreenshotCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBScreenshotCommands.h; sourceTree = ""; }; + EE9AB75F1CAEDF0C008C271F /* FBScreenshotCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBScreenshotCommands.m; sourceTree = ""; }; + EE9AB7601CAEDF0C008C271F /* FBSessionCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSessionCommands.h; sourceTree = ""; }; + EE9AB7611CAEDF0C008C271F /* FBSessionCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSessionCommands.m; sourceTree = ""; }; + EE9AB7621CAEDF0C008C271F /* FBTouchIDCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBTouchIDCommands.h; sourceTree = ""; }; + EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBTouchIDCommands.m; sourceTree = ""; }; + EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBUnknownCommands.h; sourceTree = ""; }; + EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBUnknownCommands.m; sourceTree = ""; }; + EE9AB7731CAEDF0C008C271F /* WebDriverAgent.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = WebDriverAgent.bundle; sourceTree = ""; }; + EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCommandHandler.h; sourceTree = ""; }; + EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCommandStatus.h; sourceTree = ""; }; + EE9AB7791CAEDF0C008C271F /* FBElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElement.h; sourceTree = ""; }; + EE9AB77B1CAEDF0C008C271F /* FBElementCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElementCache.h; sourceTree = ""; }; + EE9AB7801CAEDF0C008C271F /* FBResponseJSONPayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBResponseJSONPayload.h; sourceTree = ""; }; + EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBResponseJSONPayload.m; sourceTree = ""; }; + EE9AB7821CAEDF0C008C271F /* FBResponsePayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBResponsePayload.h; sourceTree = ""; }; + EE9AB7831CAEDF0C008C271F /* FBResponsePayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBResponsePayload.m; sourceTree = ""; }; + EE9AB7841CAEDF0C008C271F /* FBRoute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBRoute.h; sourceTree = ""; }; + EE9AB7851CAEDF0C008C271F /* FBRoute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRoute.m; sourceTree = ""; }; + EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBRouteRequest-Private.h"; sourceTree = ""; }; + EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBRouteRequest.h; sourceTree = ""; }; + EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRouteRequest.m; sourceTree = ""; }; + EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FBSession-Private.h"; sourceTree = ""; }; + EE9AB78A1CAEDF0C008C271F /* FBSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSession.h; sourceTree = ""; }; + EE9AB78B1CAEDF0C008C271F /* FBSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSession.m; sourceTree = ""; }; + EE9AB78C1CAEDF0C008C271F /* FBWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBWebServer.h; sourceTree = ""; }; + EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBWebServer.m; sourceTree = ""; }; + EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElementTypeTransformer.h; sourceTree = ""; }; + EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementTypeTransformer.m; sourceTree = ""; }; + EE9AB7911CAEDF0C008C271F /* FBRuntimeUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBRuntimeUtils.h; sourceTree = ""; }; + EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtils.m; sourceTree = ""; }; + EE9AB7FC1CAEE048008C271F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = WebDriverAgentRunner/Info.plist; sourceTree = SOURCE_ROOT; }; + EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UITestingUITests.m; path = WebDriverAgentRunner/UITestingUITests.m; sourceTree = SOURCE_ROOT; }; + EE9AB8031CAEE182008C271F /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; + EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EE9B76571CF7987300275851 /* FBRouteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBRouteTests.m; sourceTree = ""; }; + EE9B76581CF7987300275851 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EE9B76821CF7997600275851 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + EE9B76831CF7997600275851 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + EE9B76841CF7997600275851 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + EE9B76851CF7997600275851 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + EE9B76861CF7997600275851 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EE9B76871CF7997600275851 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + EE9B768D1CF7997600275851 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + EE9B76991CF799F400275851 /* FBAlertTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBAlertTests.m; sourceTree = ""; }; + EE9B76A11CF7A43900275851 /* FBConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = FBConfiguration.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + EE9B76A21CF7A43900275851 /* FBConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBConfiguration.m; sourceTree = ""; }; + EE9B76A31CF7A43900275851 /* FBLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBLogger.h; sourceTree = ""; }; + EE9B76A41CF7A43900275851 /* FBLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBLogger.m; sourceTree = ""; }; + EE9B76A51CF7A43900275851 /* FBMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBMacros.h; sourceTree = ""; }; + EEBBD4891D47746D00656A81 /* XCUIElement+FBFind.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBFind.h"; sourceTree = ""; }; + EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBFind.m"; sourceTree = ""; }; + EEBBD48D1D4785FC00656A81 /* XCUIElementFBFindTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIElementFBFindTests.m; sourceTree = ""; }; + EEBBDB9A1D1032F0000738CD /* XCElementSnapshotHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCElementSnapshotHelperTests.m; sourceTree = ""; }; + EEC088E41CB56AC000B65968 /* FBElementCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementCache.m; sourceTree = ""; }; + EEC088E61CB56DA400B65968 /* FBExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBExceptionHandler.h; sourceTree = ""; }; + EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBExceptionHandler.m; sourceTree = ""; }; + EEDBEBBA1CB2681900A790A2 /* WebDriverAgent.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = WebDriverAgent.bundle; sourceTree = ""; }; + EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBHealthCheck.h"; sourceTree = ""; }; + EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBHealthCheck.m"; sourceTree = ""; }; + EEDFE1231D9C08C700E6FFE5 /* XCUIDeviceHealthCheckTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIDeviceHealthCheckTests.m; sourceTree = ""; }; + EEE16E961D33A25500172525 /* FBConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBConfigurationTests.m; sourceTree = ""; }; + EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBRotation.h"; sourceTree = ""; }; + EEE3763E1D59F81400ED88DD /* XCUIDevice+FBRotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBRotation.m"; sourceTree = ""; }; + EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBUtilities.h"; sourceTree = ""; }; + EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBUtilities.m"; sourceTree = ""; }; + EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBWebDriverAttributes.h"; sourceTree = ""; }; + EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBWebDriverAttributes.m"; sourceTree = ""; }; + EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IOSSettings.xcconfig; sourceTree = ""; }; + EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBRunLoopSpinner.h; sourceTree = ""; }; + EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRunLoopSpinner.m; sourceTree = ""; }; + EEF9882A1C486603005CA669 /* yolo.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = yolo.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 641EE2D72240BBE300173FCB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 641EE6FC2240C5FD00173FCB /* WebDriverAgentLib_tvOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 641EE6282240C5CA00173FCB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7155B414224D5B170042A993 /* XCTest.framework in Frameworks */, + 7155B41C224D5B5D0042A993 /* libxml2.tbd in Frameworks */, + 7155B41B224D5B5A0042A993 /* libAccessibility.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 64B264F6228C50E0002A5025 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 64B264FE228C50E0002A5025 /* WebDriverAgentLib_tvOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE158A951CBD452B00A3E3F0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7155B40E224D5A850042A993 /* libAccessibility.tbd in Frameworks */, + 7155B424224D5BA10042A993 /* XCTest.framework in Frameworks */, + 716C9347224D540C004B8542 /* libxml2.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE2202151ECC612200A29571 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 715D5776224DE06500DA2D99 /* libxml2.tbd in Frameworks */, + EE2202171ECC612200A29571 /* WebDriverAgentLib.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE5095F71EBCC9090028E2FE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 715D5775224DE05C00DA2D99 /* libxml2.tbd in Frameworks */, + EE5095F91EBCC9090028E2FE /* WebDriverAgentLib.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE836BFF1C0F118600D87246 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 715D5773224DE02E00DA2D99 /* libxml2.tbd in Frameworks */, + AD8D96F21D3C12990061268E /* WebDriverAgentLib.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75D11CF7956C00275851 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75E91CF7956C00275851 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 715D5774224DE05400DA2D99 /* libxml2.tbd in Frameworks */, + EE9B76A01CF79C0F00275851 /* WebDriverAgentLib.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EEF988271C486603005CA669 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE158B5A1CBD462100A3E3F0 /* WebDriverAgentLib.framework in Frameworks */, + 716C9346224D540C004B8542 /* libxml2.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 498495C81BB2E6FA009CC848 /* Resources */ = { + isa = PBXGroup; + children = ( + EEDBEBBA1CB2681900A790A2 /* WebDriverAgent.bundle */, + ); + path = Resources; + sourceTree = ""; + }; + 648C10A922AAAD7600B81B9A /* UIKitCore */ = { + isa = PBXGroup; + children = ( + 648C10AA22AAAD9C00B81B9A /* UIKeyboardImpl.h */, + ); + path = UIKitCore; + sourceTree = ""; + }; + 648C10AD22AAAE2400B81B9A /* TextInput */ = { + isa = PBXGroup; + children = ( + 648C10AE22AAAE4000B81B9A /* TIPreferencesController.h */, + ); + path = TextInput; + sourceTree = ""; + }; + 6496A5D7230D6E9D0087F8CB /* AccessibilityUtilities */ = { + isa = PBXGroup; + children = ( + 6496A5D8230D6EB30087F8CB /* AXSettings.h */, + ); + path = AccessibilityUtilities; + sourceTree = ""; + }; + 64B264E8228C4D54002A5025 /* UnitTests_tvOS */ = { + isa = PBXGroup; + children = ( + 64B26505228C54C9002A5025 /* Doubles */, + 64B264EB228C4D54002A5025 /* Info.plist */, + 64B264F3228C5098002A5025 /* FBTVNavigationTrackerTests.m */, + ); + path = UnitTests_tvOS; + sourceTree = ""; + }; + 64B26505228C54C9002A5025 /* Doubles */ = { + isa = PBXGroup; + children = ( + 64B26506228C54F2002A5025 /* XCUIElementDouble.h */, + 64B26507228C5514002A5025 /* XCUIElementDouble.m */, + ); + path = Doubles; + sourceTree = ""; + }; + 71414ECF2670A1ED003A8C5D /* LRUCache */ = { + isa = PBXGroup; + children = ( + 71414ED02670A1ED003A8C5D /* LRUCache.h */, + 71414ED12670A1ED003A8C5D /* LRUCacheNode.h */, + 71414ED22670A1ED003A8C5D /* LRUCache.m */, + 71414ED32670A1ED003A8C5D /* LRUCacheNode.m */, + ); + path = LRUCache; + sourceTree = ""; + }; + 716C9340224D5358004B8542 /* tvOS */ = { + isa = PBXGroup; + children = ( + 7155B425224D5C130042A993 /* XCTAutomationSupport.framework */, + 7155B41A224D5B480042A993 /* libAccessibility.tbd */, + 7155B419224D5B460042A993 /* libxml2.tbd */, + 716C9342224D53A1004B8542 /* XCTest.framework */, + ); + name = tvOS; + sourceTree = ""; + }; + 716C9341224D5369004B8542 /* iOS */ = { + isa = PBXGroup; + children = ( + 7155B423224D5B980042A993 /* XCTest.framework */, + 716C9344224D53FC004B8542 /* XCTAutomationSupport.framework */, + 716C9343224D53DF004B8542 /* libAccessibility.tbd */, + 716C9345224D540C004B8542 /* libxml2.tbd */, + ); + name = iOS; + sourceTree = ""; + }; + 7182268F2587432E00661B83 /* CocoaAsyncSocket */ = { + isa = PBXGroup; + children = ( + 718226C72587443600661B83 /* GCDAsyncSocket.h */, + 718226C82587443600661B83 /* GCDAsyncSocket.m */, + 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */, + 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */, + ); + name = CocoaAsyncSocket; + sourceTree = ""; + }; + 91F9DAE01B99DBC2001349B2 = { + isa = PBXGroup; + children = ( + EEE5CABE1C80361500CBBDD9 /* Configurations */, + 91F9DB731B99DDD8001349B2 /* PrivateHeaders */, + 498495C81BB2E6FA009CC848 /* Resources */, + EEC288F81BF0ED2500B4DC79 /* WebDriverAgentLib */, + EE9B75F91CF7964100275851 /* WebDriverAgentTests */, + EEF988341C486655005CA669 /* WebDriverAgentRunner */, + EE9AB8021CAEE182008C271F /* Scripts */, + 91F9DAEA1B99DBC2001349B2 /* Products */, + B6E83A410C45944B036B6B0F /* Frameworks */, + AD42DD291CF121E600806E5D /* Modules */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 91F9DAEA1B99DBC2001349B2 /* Products */ = { + isa = PBXGroup; + children = ( + EE836C021C0F118600D87246 /* UnitTests.xctest */, + EEF9882A1C486603005CA669 /* yolo.xctest */, + EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */, + EE9B75D41CF7956C00275851 /* IntegrationApp.app */, + EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */, + EE5095FE1EBCC9090028E2FE /* IntegrationTests_2.xctest */, + EE22021C1ECC612200A29571 /* IntegrationTests_3.xctest */, + 641EE2DA2240BBE300173FCB /* WebDriverAgentRunner_tvOS.xctest */, + 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */, + 64B264F9228C50E0002A5025 /* UnitTests_tvOS.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 91F9DB731B99DDD8001349B2 /* PrivateHeaders */ = { + isa = PBXGroup; + children = ( + 6496A5D7230D6E9D0087F8CB /* AccessibilityUtilities */, + C8FB547222D3948300B69954 /* MobileCoreServices */, + 648C10AD22AAAE2400B81B9A /* TextInput */, + 648C10A922AAAD7600B81B9A /* UIKitCore */, + EED030DB1BFA3461007EDC1D /* XCTest */, + ); + path = PrivateHeaders; + sourceTree = ""; + }; + AD42DD291CF121E600806E5D /* Modules */ = { + isa = PBXGroup; + children = ( + AD42DD2A1CF121E600806E5D /* module.modulemap */, + ); + path = Modules; + sourceTree = ""; + }; + ADBC39951D07840300327304 /* Doubles */ = { + isa = PBXGroup; + children = ( + 13FFF2F0287DBEE600E561E4 /* XCElementSnapshotDouble.h */, + 13FFF2F1287DBEE600E561E4 /* XCElementSnapshotDouble.m */, + ADBC39961D07842800327304 /* XCUIElementDouble.h */, + ADBC39971D07842800327304 /* XCUIElementDouble.m */, + EE6A89271D0B257B0083E92B /* XCUIApplicationDouble.h */, + EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */, + ); + path = Doubles; + sourceTree = ""; + }; + B6E83A410C45944B036B6B0F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 716C9341224D5369004B8542 /* iOS */, + 716C9340224D5358004B8542 /* tvOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + C8FB547222D3948300B69954 /* MobileCoreServices */ = { + isa = PBXGroup; + children = ( + C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */, + ); + path = MobileCoreServices; + sourceTree = ""; + }; + E444DC4A24912EC40060D7EB /* Vendor */ = { + isa = PBXGroup; + children = ( + 7182268F2587432E00661B83 /* CocoaAsyncSocket */, + E444DC9E24913C080060D7EB /* RoutingHTTPServer */, + E444DC52249131050060D7EB /* CocoaHTTPServer */, + ); + name = Vendor; + sourceTree = ""; + }; + E444DC52249131050060D7EB /* CocoaHTTPServer */ = { + isa = PBXGroup; + children = ( + E444DC89249131D30060D7EB /* HTTPConnection.h */, + E444DC8C249131D30060D7EB /* HTTPConnection.m */, + E444DC8D249131D30060D7EB /* HTTPLogging.h */, + E444DC87249131D30060D7EB /* HTTPMessage.h */, + E444DC91249131D40060D7EB /* HTTPMessage.m */, + E444DC8F249131D40060D7EB /* HTTPResponse.h */, + E444DC8B249131D30060D7EB /* HTTPServer.h */, + E444DC90249131D40060D7EB /* HTTPServer.m */, + E444DC55249131740060D7EB /* Responses */, + E444DC53249131640060D7EB /* Categories */, + ); + name = CocoaHTTPServer; + sourceTree = ""; + }; + E444DC53249131640060D7EB /* Categories */ = { + isa = PBXGroup; + children = ( + E444DC7D249131B00060D7EB /* DDNumber.h */, + E444DC7F249131B00060D7EB /* DDNumber.m */, + E444DC7B249131B00060D7EB /* DDRange.h */, + E444DC7E249131B00060D7EB /* DDRange.m */, + ); + name = Categories; + sourceTree = ""; + }; + E444DC55249131740060D7EB /* Responses */ = { + isa = PBXGroup; + children = ( + E444DC60249131890060D7EB /* HTTPDataResponse.h */, + E444DC5B249131880060D7EB /* HTTPDataResponse.m */, + E444DC59249131880060D7EB /* HTTPErrorResponse.h */, + E444DC61249131890060D7EB /* HTTPErrorResponse.m */, + ); + name = Responses; + sourceTree = ""; + }; + E444DC9E24913C080060D7EB /* RoutingHTTPServer */ = { + isa = PBXGroup; + children = ( + E444DCA224913C210060D7EB /* HTTPResponseProxy.h */, + E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */, + E444DCA324913C210060D7EB /* Route.h */, + E444DCA024913C210060D7EB /* Route.m */, + E444DCAA24913C220060D7EB /* RouteRequest.h */, + E444DCA924913C220060D7EB /* RouteRequest.m */, + E444DCA124913C210060D7EB /* RouteResponse.h */, + E444DCA424913C210060D7EB /* RouteResponse.m */, + E444DCA524913C210060D7EB /* RoutingConnection.h */, + E444DCA624913C210060D7EB /* RoutingConnection.m */, + E444DCA724913C210060D7EB /* RoutingHTTPServer.h */, + E444DCA824913C220060D7EB /* RoutingHTTPServer.m */, + ); + name = RoutingHTTPServer; + sourceTree = ""; + }; + EE9AB73E1CAEDF0C008C271F /* Categories */ = { + isa = PBXGroup; + children = ( + 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */, + 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */, + 71555A3B1DEC460A007D4A8B /* NSExpression+FBFormat.h */, + 71555A3C1DEC460A007D4A8B /* NSExpression+FBFormat.m */, + 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */, + 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */, + EE0D1F5F1EBCDCF7006A3123 /* NSString+FBVisualLength.h */, + EE0D1F601EBCDCF7006A3123 /* NSString+FBVisualLength.m */, + 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */, + 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */, + 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */, + 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */, + 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */, + 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */, + AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */, + AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */, + 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */, + 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */, + 716C9DFE27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h */, + 716C9DFF27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m */, + 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */, + 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */, + 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */, + 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */, + 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */, + 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */, + EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */, + EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */, + AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */, + AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */, + EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */, + EEE3763E1D59F81400ED88DD /* XCUIDevice+FBRotation.m */, + EE9AB7451CAEDF0C008C271F /* XCUIElement+FBAccessibility.h */, + EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */, + 71D04DC625356C43008A052C /* XCUIElement+FBCaching.h */, + 71D04DC725356C43008A052C /* XCUIElement+FBCaching.m */, + 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */, + 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */, + EEBBD4891D47746D00656A81 /* XCUIElement+FBFind.h */, + EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */, + EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */, + EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */, + EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */, + EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */, + 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */, + 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */, + 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */, + 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */, + 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */, + 71D3B3D4267FC7260076473D /* XCUIElement+FBResolve.m */, + EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */, + EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */, + 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */, + 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */, + AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */, + AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */, + 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */, + 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */, + EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */, + EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */, + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */, + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */, + EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */, + EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */, + 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */, + 641EE7072240CDEB00173FCB /* XCUIElement+FBTVFocuse.m */, + 71E75E6B254824230099FC87 /* XCUIElementQuery+FBHelpers.h */, + 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */, + 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */, + 13DE7A5A287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m */, + ); + name = Categories; + path = WebDriverAgentLib/Categories; + sourceTree = SOURCE_ROOT; + }; + EE9AB74F1CAEDF0C008C271F /* Commands */ = { + isa = PBXGroup; + children = ( + EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */, + EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */, + EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */, + EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */, + EE9AB7541CAEDF0C008C271F /* FBDebugCommands.h */, + EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */, + EE9AB7561CAEDF0C008C271F /* FBElementCommands.h */, + EE9AB7571CAEDF0C008C271F /* FBElementCommands.m */, + EE9AB7581CAEDF0C008C271F /* FBFindElementCommands.h */, + EE9AB7591CAEDF0C008C271F /* FBFindElementCommands.m */, + EE9AB75C1CAEDF0C008C271F /* FBOrientationCommands.h */, + EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */, + EE9AB75E1CAEDF0C008C271F /* FBScreenshotCommands.h */, + EE9AB75F1CAEDF0C008C271F /* FBScreenshotCommands.m */, + EE9AB7601CAEDF0C008C271F /* FBSessionCommands.h */, + EE9AB7611CAEDF0C008C271F /* FBSessionCommands.m */, + 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */, + 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */, + EE9AB7621CAEDF0C008C271F /* FBTouchIDCommands.h */, + EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */, + EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */, + EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */, + 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */, + 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */, + ); + name = Commands; + path = WebDriverAgentLib/Commands; + sourceTree = SOURCE_ROOT; + }; + EE9AB7721CAEDF0C008C271F /* Resources */ = { + isa = PBXGroup; + children = ( + EE9AB7731CAEDF0C008C271F /* WebDriverAgent.bundle */, + ); + name = Resources; + path = WebDriverAgentLib/Resources; + sourceTree = SOURCE_ROOT; + }; + EE9AB7741CAEDF0C008C271F /* Routing */ = { + isa = PBXGroup; + children = ( + EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */, + EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */, + 71B155DB230711E900646AFB /* FBCommandStatus.m */, + EE9AB7791CAEDF0C008C271F /* FBElement.h */, + EE9AB77B1CAEDF0C008C271F /* FBElementCache.h */, + EEC088E41CB56AC000B65968 /* FBElementCache.m */, + 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */, + 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */, + 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */, + 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */, + EEC088E61CB56DA400B65968 /* FBExceptionHandler.h */, + EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */, + 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */, + EE9AB7801CAEDF0C008C271F /* FBResponseJSONPayload.h */, + EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */, + EE9AB7821CAEDF0C008C271F /* FBResponsePayload.h */, + EE9AB7831CAEDF0C008C271F /* FBResponsePayload.m */, + EE9AB7841CAEDF0C008C271F /* FBRoute.h */, + EE9AB7851CAEDF0C008C271F /* FBRoute.m */, + EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */, + EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */, + EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */, + 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */, + 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */, + 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */, + 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */, + 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */, + 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */, + EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */, + EE9AB78A1CAEDF0C008C271F /* FBSession.h */, + EE9AB78B1CAEDF0C008C271F /* FBSession.m */, + 715557D1211DBCE700613B26 /* FBTCPSocket.h */, + 715557D2211DBCE700613B26 /* FBTCPSocket.m */, + EE9AB78C1CAEDF0C008C271F /* FBWebServer.h */, + EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */, + 13DE7A41287C2A8D003243C6 /* FBXCAccessibilityElement.h */, + 13DE7A42287C2A8D003243C6 /* FBXCAccessibilityElement.m */, + 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */, + 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */, + 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */, + 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */, + 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */, + 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */, + ); + name = Routing; + path = WebDriverAgentLib/Routing; + sourceTree = SOURCE_ROOT; + }; + EE9AB78E1CAEDF0C008C271F /* Utilities */ = { + isa = PBXGroup; + children = ( + 71414ECF2670A1ED003A8C5D /* LRUCache */, + 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */, + 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */, + 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */, + 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */, + 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */, + 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */, + 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */, + 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */, + 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */, + 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */, + EE9B76A11CF7A43900275851 /* FBConfiguration.h */, + EE9B76A21CF7A43900275851 /* FBConfiguration.m */, + EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */, + EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */, + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */, + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */, + EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */, + EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */, + EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */, + EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */, + EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */, + EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */, + 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */, + 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */, + 7150348521A6DAD600A0F4BA /* FBImageUtils.h */, + 7150348621A6DAD600A0F4BA /* FBImageUtils.m */, + EE9B76A31CF7A43900275851 /* FBLogger.h */, + EE9B76A41CF7A43900275851 /* FBLogger.m */, + EE9B76A51CF7A43900275851 /* FBMacros.h */, + EE1888381DA661C400307AA8 /* FBMathUtils.h */, + EE1888391DA661C400307AA8 /* FBMathUtils.m */, + 7155D701211DCEF400166C20 /* FBMjpegServer.h */, + 7155D702211DCEF400166C20 /* FBMjpegServer.m */, + 719DCF132601EAFB000E765F /* FBNotificationsHelper.h */, + 719DCF142601EAFB000E765F /* FBNotificationsHelper.m */, + 71930C4020662E1F00D3AFEC /* FBPasteboard.h */, + 71930C4120662E1F00D3AFEC /* FBPasteboard.m */, + 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */, + 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */, + 716C9DF827315D21005AD475 /* FBReflectionUtils.h */, + 716C9DF927315D21005AD475 /* FBReflectionUtils.m */, + EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */, + EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */, + EE9AB7911CAEDF0C008C271F /* FBRuntimeUtils.h */, + EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */, + 71F3E7D225417FF400E0C22B /* FBSettings.h */, + 71F3E7D325417FF400E0C22B /* FBSettings.m */, + 715AFABF1FFA29180053896D /* FBScreen.h */, + 715AFAC01FFA29180053896D /* FBScreen.m */, + 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */, + 71C9EAAB25E8415A00470CD8 /* FBScreenshot.m */, + 641EE70A2240CE2D00173FCB /* FBTVNavigationTracker.h */, + 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */, + 641EE70D2240CE4800173FCB /* FBTVNavigationTracker.m */, + C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */, + C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */, + 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */, + 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */, + 713AE573243A53BE0000D657 /* FBW3CActionsHelpers.h */, + 713AE574243A53BE0000D657 /* FBW3CActionsHelpers.m */, + 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */, + 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */, + EE5A24401F136C8D0078B1D9 /* FBXCodeCompatibility.h */, + EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */, + EE35AD791E3B80C000A02D78 /* FBXCTestDaemonsProxy.h */, + EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */, + 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */, + 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */, + 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */, + 711084421DA3AA7500F913D6 /* FBXPath.h */, + 711084431DA3AA7500F913D6 /* FBXPath.m */, + EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, + EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */, + 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */, + 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */, + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */, + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */, + ); + name = Utilities; + path = WebDriverAgentLib/Utilities; + sourceTree = SOURCE_ROOT; + }; + EE9AB8021CAEE182008C271F /* Scripts */ = { + isa = PBXGroup; + children = ( + EE9AB8031CAEE182008C271F /* build.sh */, + ); + path = Scripts; + sourceTree = ""; + }; + EE9B75F91CF7964100275851 /* WebDriverAgentTests */ = { + isa = PBXGroup; + children = ( + 64B264E8228C4D54002A5025 /* UnitTests_tvOS */, + EE9B76801CF7997600275851 /* IntegrationApp */, + EE9B76541CF7987300275851 /* IntegrationTests */, + EE9B76561CF7987300275851 /* UnitTests */, + ); + path = WebDriverAgentTests; + sourceTree = ""; + }; + EE9B76541CF7987300275851 /* IntegrationTests */ = { + isa = PBXGroup; + children = ( + EE9B76991CF799F400275851 /* FBAlertTests.m */, + 719CD8FE2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m */, + 644D9CCD230E1F1A00C90459 /* FBConfigurationTests.m */, + EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */, + 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */, + EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */, + EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */, + EE8DDD7A20C57320004D4925 /* FBForceTouchTests.m */, + 631B523421F6174300625362 /* FBImageProcessorTests.m */, + EE1E06DB1D18090F007CF043 /* FBIntegrationTestCase.h */, + EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */, + EE05BAF91D13003C00A3EB00 /* FBKeyboardTests.m */, + 71930C462066434000D3AFEC /* FBPasteboardTests.m */, + 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */, + 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */, + 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */, + EE55B3261D1D54CF003AAAEC /* FBScrollingTests.m */, + 7152EB2F1F41F9960047EEFF /* FBSessionIntegrationTests.m */, + EE26409A1D0EB5E8009BE6B0 /* FBTapTest.m */, + EE1E06DC1D1811C4007CF043 /* FBTestMacros.h */, + AD76723F1D6B826F00610457 /* FBTypingTest.m */, + 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */, + 71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */, + 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */, + 7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */, + 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */, + EEBBDB9A1D1032F0000738CD /* XCElementSnapshotHelperTests.m */, + EE006EB21EBA1C7B006900A4 /* XCElementSnapshotHitPointTests.m */, + EE1E06E31D18213F007CF043 /* XCUIApplicationHelperTests.m */, + EEDFE1231D9C08C700E6FFE5 /* XCUIDeviceHealthCheckTests.m */, + EE1E06DF1D181BB4007CF043 /* XCUIDeviceHelperTests.m */, + 44757A831D42CE8300ECF35E /* XCUIDeviceRotationTests.m */, + 71E504941DF59BAD0020C32A /* XCUIElementAttributesTests.m */, + EEBBD48D1D4785FC00656A81 /* XCUIElementFBFindTests.m */, + EE1E06E11D181CC9007CF043 /* XCUIElementHelperIntegrationTests.m */, + ); + path = IntegrationTests; + sourceTree = ""; + }; + EE9B76561CF7987300275851 /* UnitTests */ = { + isa = PBXGroup; + children = ( + ADBC39951D07840300327304 /* Doubles */, + 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */, + EEE16E961D33A25500172525 /* FBConfigurationTests.m */, + ADBC39931D0782CD00327304 /* FBElementCacheTests.m */, + EE3F8CFF1D08B05F006F02CE /* FBElementTypeTransformerTests.m */, + 719FF5B81DAD21F5008E0099 /* FBElementUtilitiesTests.m */, + EE6A892C1D0B2AF40083E92B /* FBErrorBuilderTests.m */, + 715D554A2229891B00524509 /* FBExceptionHandlerTests.m */, + 713352FC26CEF31D00523CBC /* FBLRUCacheTests.m */, + EE18883C1DA663EB00307AA8 /* FBMathUtilsTests.m */, + 718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */, + EE9B76571CF7987300275851 /* FBRouteTests.m */, + EE3F8CFD1D08AA17006F02CE /* FBRunLoopSpinnerTests.m */, + ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */, + 714801D01FA9D9FA00DC5997 /* FBSDKVersionTests.m */, + EE6A89251D0B19E60083E92B /* FBSessionTests.m */, + 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */, + 712A0C841DA3E459007D02E5 /* FBXPathTests.m */, + EE9B76581CF7987300275851 /* Info.plist */, + 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */, + 7139145B1DF01A12005896C2 /* NSExpressionFBFormatTests.m */, + 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */, + 713914591DF01989005896C2 /* XCUIElementHelpersTests.m */, + ); + path = UnitTests; + sourceTree = ""; + }; + EE9B76801CF7997600275851 /* IntegrationApp */ = { + isa = PBXGroup; + children = ( + EE9B76811CF7997600275851 /* Classes */, + EE9B76881CF7997600275851 /* Resources */, + EE9B76861CF7997600275851 /* Info.plist */, + EE9B76871CF7997600275851 /* main.m */, + ); + path = IntegrationApp; + sourceTree = ""; + }; + EE9B76811CF7997600275851 /* Classes */ = { + isa = PBXGroup; + children = ( + EE9B76821CF7997600275851 /* AppDelegate.h */, + EE9B76831CF7997600275851 /* AppDelegate.m */, + EE1E06E51D182E95007CF043 /* FBAlertViewController.h */, + EE1E06E61D182E95007CF043 /* FBAlertViewController.m */, + EE8BA9781DCCED9A00A9DEF8 /* FBNavigationController.h */, + EE8BA9791DCCED9A00A9DEF8 /* FBNavigationController.m */, + ADDA07221D6BB2BF001700AC /* FBScrollViewController.h */, + ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */, + EE55B3221D1D5388003AAAEC /* FBTableDataSource.h */, + EE55B3231D1D5388003AAAEC /* FBTableDataSource.m */, + EE9B76841CF7997600275851 /* ViewController.h */, + EE9B76851CF7997600275851 /* ViewController.m */, + 315A14FF2518CB8700A3A064 /* TouchableView.h */, + 315A15002518CB8700A3A064 /* TouchableView.m */, + 315A15052518CC2800A3A064 /* TouchSpotView.h */, + 315A15062518CC2800A3A064 /* TouchSpotView.m */, + 315A15082518D6F400A3A064 /* TouchViewController.h */, + 315A15092518D6F400A3A064 /* TouchViewController.m */, + ); + path = Classes; + sourceTree = ""; + }; + EE9B76881CF7997600275851 /* Resources */ = { + isa = PBXGroup; + children = ( + EE9B768C1CF7997600275851 /* Main.storyboard */, + ); + path = Resources; + sourceTree = ""; + }; + EEC288F81BF0ED2500B4DC79 /* WebDriverAgentLib */ = { + isa = PBXGroup; + children = ( + E444DC4A24912EC40060D7EB /* Vendor */, + EE9AB73E1CAEDF0C008C271F /* Categories */, + EE9AB74F1CAEDF0C008C271F /* Commands */, + EE9AB7721CAEDF0C008C271F /* Resources */, + EE9AB7741CAEDF0C008C271F /* Routing */, + EE9AB78E1CAEDF0C008C271F /* Utilities */, + EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */, + AD6C26921CF2379700F8B5FF /* FBAlert.h */, + AD6C26931CF2379700F8B5FF /* FBAlert.m */, + EE3A18641CDE734B00DE4205 /* FBKeyboard.h */, + EE3A18651CDE734B00DE4205 /* FBKeyboard.m */, + EE158B5D1CBD479000A3E3F0 /* Info.plist */, + ); + name = WebDriverAgentLib; + path = XCTWebDriverAgentLib; + sourceTree = ""; + }; + EED030DB1BFA3461007EDC1D /* XCTest */ = { + isa = PBXGroup; + children = ( + EE35AC981E3B77D600A02D78 /* _XCInternalTestRun.h */, + EE35AC991E3B77D600A02D78 /* _XCKVOExpectationImplementation.h */, + EE35AC9A1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h */, + EE35AC9B1E3B77D600A02D78 /* _XCTestCaseImplementation.h */, + EE35AC9C1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h */, + EE35AC9D1E3B77D600A02D78 /* _XCTestExpectationImplementation.h */, + EE35AC9E1E3B77D600A02D78 /* _XCTestImplementation.h */, + EE35AC9F1E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h */, + EE35ACA01E3B77D600A02D78 /* _XCTestSuiteImplementation.h */, + EE35ACA11E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h */, + EE35ACA21E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h */, + EE35ACA31E3B77D600A02D78 /* _XCTWaiterImpl.h */, + EE35ACA41E3B77D600A02D78 /* CDStructures.h */, + EE35ACAB1E3B77D600A02D78 /* NSString-XCTAdditions.h */, + EE35ACAC1E3B77D600A02D78 /* NSValue-XCTestAdditions.h */, + EE35ACAD1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h */, + EE35ACAE1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h */, + EE35ACAF1E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h */, + EE35ACB01E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h */, + EE35ACB11E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h */, + EE35ACB21E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h */, + EE35ACB41E3B77D600A02D78 /* XCActivityRecord.h */, + EE35ACB51E3B77D600A02D78 /* XCApplicationMonitor_iOS.h */, + EE35ACB61E3B77D600A02D78 /* XCApplicationMonitor.h */, + EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */, + EE35ACB81E3B77D600A02D78 /* XCAXClient_iOS.h */, + EE35ACB91E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h */, + EE35ACBD1E3B77D600A02D78 /* XCEventGenerator.h */, + EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */, + EE35ACBF1E3B77D600A02D78 /* XCKeyboardKeyMap.h */, + EE35ACC01E3B77D600A02D78 /* XCKeyboardLayout.h */, + EE35ACC11E3B77D600A02D78 /* XCKeyMappingPath.h */, + EE35ACC21E3B77D600A02D78 /* XCPointerEvent.h */, + EE35ACC31E3B77D600A02D78 /* XCPointerEventPath.h */, + EE35ACC41E3B77D600A02D78 /* XCSourceCodeRecording.h */, + EE35ACC51E3B77D600A02D78 /* XCSourceCodeTreeNode.h */, + EE35ACC61E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h */, + EE35ACC71E3B77D600A02D78 /* XCSymbolicationRecord.h */, + EE35ACC81E3B77D600A02D78 /* XCSymbolicatorHolder.h */, + EE35ACC91E3B77D600A02D78 /* XCSynthesizedEventRecord.h */, + EE35ACCA1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h */, + EE35ACCB1E3B77D600A02D78 /* XCTAsyncActivity.h */, + EE35ACCC1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h */, + EE35ACCD1E3B77D600A02D78 /* XCTAXClient-Protocol.h */, + EE35ACCE1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h */, + EE35ACCF1E3B77D600A02D78 /* XCTest.h */, + EE35ACD01E3B77D600A02D78 /* XCTestCase.h */, + EE35ACD11E3B77D600A02D78 /* XCTestCaseRun.h */, + EE35ACD21E3B77D600A02D78 /* XCTestCaseSuite.h */, + EE35ACD31E3B77D600A02D78 /* XCTestConfiguration.h */, + EE35ACD41E3B77D600A02D78 /* XCTestContext.h */, + EE35ACD51E3B77D600A02D78 /* XCTestContextScope.h */, + EE35ACD61E3B77D600A02D78 /* XCTestDriver.h */, + EE35ACD71E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h */, + EE35ACD81E3B77D600A02D78 /* XCTestExpectation.h */, + EE35ACD91E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h */, + EE35ACDA1E3B77D600A02D78 /* XCTestExpectationWaiter.h */, + EE35ACDB1E3B77D600A02D78 /* XCTestLog.h */, + EE35ACDC1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h */, + EE35ACDD1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h */, + EE35ACDE1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h */, + EE35ACDF1E3B77D600A02D78 /* XCTestMisuseObserver.h */, + EE35ACE01E3B77D600A02D78 /* XCTestObservation-Protocol.h */, + EE35ACE11E3B77D600A02D78 /* XCTestObservationCenter.h */, + EE35ACE21E3B77D600A02D78 /* XCTestObserver.h */, + EE35ACE31E3B77D600A02D78 /* XCTestProbe.h */, + EE35ACE41E3B77D600A02D78 /* XCTestRun.h */, + EE35ACE51E3B77D600A02D78 /* XCTestSuite.h */, + EE35ACE61E3B77D600A02D78 /* XCTestSuiteRun.h */, + EE35ACE71E3B77D600A02D78 /* XCTestWaiter.h */, + EE35ACE81E3B77D600A02D78 /* XCTKVOExpectation.h */, + EE35ACE91E3B77D600A02D78 /* XCTMetric.h */, + EE35ACEA1E3B77D600A02D78 /* XCTNSNotificationExpectation.h */, + EE35ACEB1E3B77D600A02D78 /* XCTNSPredicateExpectation.h */, + EE35ACEC1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h */, + EE35ACEE1E3B77D600A02D78 /* XCTRunnerAutomationSession.h */, + EE35ACEF1E3B77D600A02D78 /* XCTRunnerDaemonSession.h */, + EE35ACF01E3B77D600A02D78 /* XCTRunnerIDESession.h */, + EE35ACF11E3B77D600A02D78 /* XCTTestRunSession.h */, + EE35ACF21E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h */, + EE35ACF31E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h */, + EE35ACF41E3B77D600A02D78 /* XCTWaiter.h */, + EE35ACF51E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h */, + EE35ACF61E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h */, + EE35ACF71E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h */, + EE35ACF81E3B77D600A02D78 /* XCTWaiterManager.h */, + EE35ACF91E3B77D600A02D78 /* XCUIApplication.h */, + EE35ACFA1E3B77D600A02D78 /* XCUIApplicationImpl.h */, + EE35ACFB1E3B77D600A02D78 /* XCUIApplicationProcess.h */, + EE35ACFC1E3B77D600A02D78 /* XCUICoordinate.h */, + EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */, + EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */, + EE35ACFF1E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h */, + EE35AD011E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h */, + EE35AD021E3B77D600A02D78 /* XCUIElementQuery.h */, + 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */, + EE35AD041E3B77D600A02D78 /* XCUIRecorderNodeFinder.h */, + EE35AD051E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h */, + EE35AD061E3B77D600A02D78 /* XCUIRecorderTimingMessage.h */, + EE35AD071E3B77D600A02D78 /* XCUIRecorderUtilities.h */, + 1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */, + 7119097B2152580600BA3C7E /* XCUIScreen.h */, + 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */, + ); + path = XCTest; + sourceTree = ""; + }; + EEE5CABE1C80361500CBBDD9 /* Configurations */ = { + isa = PBXGroup; + children = ( + 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */, + 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */, + 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */, + EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */, + ); + path = Configurations; + sourceTree = ""; + }; + EEF988341C486655005CA669 /* WebDriverAgentRunner */ = { + isa = PBXGroup; + children = ( + EE9AB7FC1CAEE048008C271F /* Info.plist */, + EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */, + ); + name = WebDriverAgentRunner; + path = XCTUITestRunner; + sourceTree = SOURCE_ROOT; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 641EE6302240C5CA00173FCB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 641EE6312240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.h in Headers */, + 7182274A258744BE00661B83 /* HTTPMessage.h in Headers */, + 641EE6322240C5CA00173FCB /* FBScreen.h in Headers */, + 641EE6332240C5CA00173FCB /* XCTestPrivateSymbols.h in Headers */, + 641EE6342240C5CA00173FCB /* XCUIElement+FBTyping.h in Headers */, + 7182270B258744A700661B83 /* Route.h in Headers */, + 641EE6352240C5CA00173FCB /* XCUIElement+FBUtilities.h in Headers */, + 641EE6362240C5CA00173FCB /* XCUIElement+FBScrolling.h in Headers */, + 1357E297233D05240054BDB8 /* XCUIHitPointResult.h in Headers */, + 716C9DFB27315D21005AD475 /* FBReflectionUtils.h in Headers */, + 641EE6372240C5CA00173FCB /* XCSourceCodeTreeNode.h in Headers */, + 641EE6382240C5CA00173FCB /* XCPointerEventPath.h in Headers */, + 641EE6392240C5CA00173FCB /* FBRouteRequest.h in Headers */, + 648C10AC22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */, + 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */, + 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, + 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, + 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */, + 641EE63A2240C5CA00173FCB /* XCTest.h in Headers */, + 641EE63B2240C5CA00173FCB /* FBAlertsMonitor.h in Headers */, + 641EE63D2240C5CA00173FCB /* FBSession.h in Headers */, + 641EE63E2240C5CA00173FCB /* _XCTestImplementation.h in Headers */, + 641EE63F2240C5CA00173FCB /* FBTouchActionCommands.h in Headers */, + 641EE6402240C5CA00173FCB /* FBTouchIDCommands.h in Headers */, + 714D88CD2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */, + 641EE6412240C5CA00173FCB /* XCUIApplication.h in Headers */, + 641EE6422240C5CA00173FCB /* FBCustomCommands.h in Headers */, + 641EE6432240C5CA00173FCB /* _XCTestCaseInterruptionException.h in Headers */, + 641EE6442240C5CA00173FCB /* FBOrientationCommands.h in Headers */, + 641EE6452240C5CA00173FCB /* XCUIScreen.h in Headers */, + 641EE6462240C5CA00173FCB /* XCTRunnerIDESession.h in Headers */, + 641EE6472240C5CA00173FCB /* FBRouteRequest-Private.h in Headers */, + 71D475C32538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h in Headers */, + 641EE6482240C5CA00173FCB /* XCTTestRunSession.h in Headers */, + 641EE6492240C5CA00173FCB /* XCTestProbe.h in Headers */, + 641EE64A2240C5CA00173FCB /* XCApplicationQuery.h in Headers */, + 641EE64B2240C5CA00173FCB /* XCTAsyncActivity.h in Headers */, + 641EE64C2240C5CA00173FCB /* XCTestMisuseObserver.h in Headers */, + 641EE64D2240C5CA00173FCB /* XCTRunnerDaemonSession.h in Headers */, + 714E14B929805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, + 64B2650B228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, + 641EE64F2240C5CA00173FCB /* XCTestExpectationWaiter.h in Headers */, + 13DE7A5C287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */, + 641EE6502240C5CA00173FCB /* UIGestureRecognizer-RecordingAdditions.h in Headers */, + 71BB58E92B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */, + 641EE6512240C5CA00173FCB /* XCKeyboardKeyMap.h in Headers */, + 641EE6522240C5CA00173FCB /* XCTNSPredicateExpectationObject-Protocol.h in Headers */, + 641EE6532240C5CA00173FCB /* WebDriverAgentLib.h in Headers */, + 641EE6542240C5CA00173FCB /* FBFindElementCommands.h in Headers */, + 641EE6552240C5CA00173FCB /* XCTestRun.h in Headers */, + 641EE6562240C5CA00173FCB /* FBWebServer.h in Headers */, + 641EE6572240C5CA00173FCB /* FBScreenshotCommands.h in Headers */, + 641EE6582240C5CA00173FCB /* _XCKVOExpectationImplementation.h in Headers */, + 641EE6592240C5CA00173FCB /* NSString+FBVisualLength.h in Headers */, + 641EE65A2240C5CA00173FCB /* FBXCTestDaemonsProxy.h in Headers */, + 641EE65B2240C5CA00173FCB /* XCUIElementHitPointCoordinate.h in Headers */, + 641EE65C2240C5CA00173FCB /* XCTDarwinNotificationExpectation.h in Headers */, + 641EE65D2240C5CA00173FCB /* XCTRunnerAutomationSession.h in Headers */, + 641EE65F2240C5CA00173FCB /* XCSourceCodeTreeNodeEnumerator.h in Headers */, + 641EE6602240C5CA00173FCB /* XCUIElement+FBIsVisible.h in Headers */, + 641EE6622240C5CA00173FCB /* FBResponsePayload.h in Headers */, + 71BB58E22B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */, + 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */, + 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, + 71822738258744B800661B83 /* HTTPConnection.h in Headers */, + 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */, + 641EE6652240C5CA00173FCB /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */, + 641EE6662240C5CA00173FCB /* XCTestCase.h in Headers */, + 641EE6672240C5CA00173FCB /* XCSymbolicatorHolder.h in Headers */, + 641EE6682240C5CA00173FCB /* XCUIApplicationImpl.h in Headers */, + 71414ED72670A1EE003A8C5D /* LRUCacheNode.h in Headers */, + 641EE6692240C5CA00173FCB /* UIPanGestureRecognizer-RecordingAdditions.h in Headers */, + 13815F702328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */, + 641EE66A2240C5CA00173FCB /* NSExpression+FBFormat.h in Headers */, + 641EE66B2240C5CA00173FCB /* _XCTestCaseImplementation.h in Headers */, + 641EE66C2240C5CA00173FCB /* UIPinchGestureRecognizer-RecordingAdditions.h in Headers */, + 641EE66D2240C5CA00173FCB /* XCTestManager_TestsInterface-Protocol.h in Headers */, + 641EE66E2240C5CA00173FCB /* XCUIApplication+FBAlert.h in Headers */, + 716C9E0127315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h in Headers */, + 7182275C258744C300661B83 /* HTTPServer.h in Headers */, + 641EE6702240C5CA00173FCB /* FBMathUtils.h in Headers */, + 641EE6712240C5CA00173FCB /* UISwipeGestureRecognizer-RecordingAdditions.h in Headers */, + 641EE6722240C5CA00173FCB /* FBElementUtils.h in Headers */, + 641EE6732240C5CA00173FCB /* FBDebugCommands.h in Headers */, + 641EE6742240C5CA00173FCB /* XCTestSuite.h in Headers */, + 641EE6752240C5CA00173FCB /* XCUICoordinate.h in Headers */, + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */, + 641EE6762240C5CA00173FCB /* XCTNSPredicateExpectation.h in Headers */, + 641EE6772240C5CA00173FCB /* XCTestObservationCenter.h in Headers */, + 641EE6782240C5CA00173FCB /* XCTNSNotificationExpectation.h in Headers */, + 641EE6792240C5CA00173FCB /* XCUIRecorderNodeFinder.h in Headers */, + 641EE67A2240C5CA00173FCB /* XCUIElement+FBAccessibility.h in Headers */, + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, + 641EE67B2240C5CA00173FCB /* XCUIRecorderUtilities.h in Headers */, + 6496A5DA230D6EB30087F8CB /* AXSettings.h in Headers */, + 641EE67C2240C5CA00173FCB /* XCTestCaseRun.h in Headers */, + 71A5C67429A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */, + 641EE67D2240C5CA00173FCB /* XCTestConfiguration.h in Headers */, + 641EE67E2240C5CA00173FCB /* _XCTDarwinNotificationExpectationImplementation.h in Headers */, + 641EE67F2240C5CA00173FCB /* XCTestExpectation.h in Headers */, + 641EE6802240C5CA00173FCB /* FBElementTypeTransformer.h in Headers */, + 641EE6812240C5CA00173FCB /* FBXCAXClientProxy.h in Headers */, + 641EE6822240C5CA00173FCB /* FBElementCache.h in Headers */, + 641EE6832240C5CA00173FCB /* XCTMetric.h in Headers */, + 641EE6842240C5CA00173FCB /* XCTestContextScope.h in Headers */, + 7182271D258744AB00661B83 /* RouteResponse.h in Headers */, + 641EE6852240C5CA00173FCB /* XCUIElement+FBClassChain.h in Headers */, + 13DE7A44287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */, + 641EE6862240C5CA00173FCB /* FBResponseJSONPayload.h in Headers */, + 71822714258744A900661B83 /* RouteRequest.h in Headers */, + 641EE6872240C5CA00173FCB /* XCTAutomationTarget-Protocol.h in Headers */, + 641EE6882240C5CA00173FCB /* FBElement.h in Headers */, + 641EE6892240C5CA00173FCB /* XCTAXClient-Protocol.h in Headers */, + 641EE68B2240C5CA00173FCB /* FBExceptionHandler.h in Headers */, + 71822726258744AE00661B83 /* RoutingConnection.h in Headers */, + 641EE68C2240C5CA00173FCB /* FBRoute.h in Headers */, + 641EE68D2240C5CA00173FCB /* XCTestDriver.h in Headers */, + 641EE68E2240C5CA00173FCB /* _XCTNSNotificationExpectationImplementation.h in Headers */, + 641EE68F2240C5CA00173FCB /* XCSynthesizedEventRecord.h in Headers */, + 641EE6922240C5CA00173FCB /* XCTWaiterDelegatePrivate-Protocol.h in Headers */, + 641EE6932240C5CA00173FCB /* XCTestManager_IDEInterface-Protocol.h in Headers */, + 71822753258744C100661B83 /* HTTPResponse.h in Headers */, + 641EE6942240C5CA00173FCB /* FBXPath.h in Headers */, + 641EE6952240C5CA00173FCB /* XCUIRecorderTimingMessage.h in Headers */, + 641EE6962240C5CA00173FCB /* XCApplicationMonitor.h in Headers */, + 641EE6972240C5CA00173FCB /* XCUIElement+FBForceTouch.h in Headers */, + 641EE6982240C5CA00173FCB /* FBRuntimeUtils.h in Headers */, + 71F5BE50252F14EB00EE9EBA /* FBExceptions.h in Headers */, + 641EE6992240C5CA00173FCB /* XCUIElement+FBPickerWheel.h in Headers */, + 641EE69A2240C5CA00173FCB /* XCTestObservation-Protocol.h in Headers */, + 641EE69B2240C5CA00173FCB /* _XCTNSPredicateExpectationImplementation.h in Headers */, + 641EE69C2240C5CA00173FCB /* FBElementCommands.h in Headers */, + 641EE69F2240C5CA00173FCB /* FBTCPSocket.h in Headers */, + 641EE6A02240C5CA00173FCB /* XCUIElement+FBUID.h in Headers */, + 641EE6A12240C5CA00173FCB /* XCSymbolicationRecord.h in Headers */, + 641EE6A22240C5CA00173FCB /* XCUIDevice.h in Headers */, + 7182272F258744B000661B83 /* RoutingHTTPServer.h in Headers */, + 641EE6A32240C5CA00173FCB /* XCUIApplication+FBTouchAction.h in Headers */, + 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */, + 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */, + 641EE70C2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, + 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */, + 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */, + 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */, + B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, + 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, + 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */, + 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */, + 71822741258744BB00661B83 /* HTTPLogging.h in Headers */, + 641EE6AB2240C5CA00173FCB /* FBAlertViewCommands.h in Headers */, + 641EE6AC2240C5CA00173FCB /* XCTWaiter.h in Headers */, + 641EE6AD2240C5CA00173FCB /* XCTWaiterManagement-Protocol.h in Headers */, + 641EE6AF2240C5CA00173FCB /* XCTestContext.h in Headers */, + 71C9EAAD25E8415A00470CD8 /* FBScreenshot.h in Headers */, + 641EE6B12240C5CA00173FCB /* XCTWaiterDelegate-Protocol.h in Headers */, + 641EE6B22240C5CA00173FCB /* _XCTestExpectationImplementation.h in Headers */, + 641EE6B32240C5CA00173FCB /* XCAXClient_iOS.h in Headers */, + 641EE6B42240C5CA00173FCB /* XCTWaiterManager.h in Headers */, + 641EE6B52240C5CA00173FCB /* XCTestDriverInterface-Protocol.h in Headers */, + 648C10B022AAAE4000B81B9A /* TIPreferencesController.h in Headers */, + 71F5BE24252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */, + 641EE6B62240C5CA00173FCB /* _XCTestSuiteImplementation.h in Headers */, + 641EE6B72240C5CA00173FCB /* FBBaseActionsSynthesizer.h in Headers */, + 7182276E258744C900661B83 /* HTTPErrorResponse.h in Headers */, + 641EE6B82240C5CA00173FCB /* FBAlert.h in Headers */, + 641EE6B92240C5CA00173FCB /* XCUIElementQuery.h in Headers */, + 71BB58F02B96511800CB9BFE /* FBVideoCommands.h in Headers */, + 641EE6BA2240C5CA00173FCB /* XCPointerEvent.h in Headers */, + 718F49C923087ACF0045FE8B /* FBProtocolHelpers.h in Headers */, + 641EE6BB2240C5CA00173FCB /* XCSourceCodeRecording.h in Headers */, + 641EE6BC2240C5CA00173FCB /* FBRunLoopSpinner.h in Headers */, + 641EE6BD2240C5CA00173FCB /* FBErrorBuilder.h in Headers */, + 641EE6BE2240C5CA00173FCB /* XCApplicationMonitor_iOS.h in Headers */, + 13DE7A4A287C4005003243C6 /* FBXCDeviceEvent.h in Headers */, + 641EE6BF2240C5CA00173FCB /* FBKeyboard.h in Headers */, + 71E75E6E254824230099FC87 /* XCUIElementQuery+FBHelpers.h in Headers */, + 641EE6C02240C5CA00173FCB /* XCUIApplication+FBHelpers.h in Headers */, + 641EE6C12240C5CA00173FCB /* _XCTestObservationCenterImplementation.h in Headers */, + 714EAA0E2673FDFE005C5B47 /* FBCapabilities.h in Headers */, + 641EE6C22240C5CA00173FCB /* XCUIDevice+FBHelpers.h in Headers */, + 71D3B3D6267FC7260076473D /* XCUIElement+FBResolve.h in Headers */, + 641EE6C32240C5CA00173FCB /* FBClassChainQueryParser.h in Headers */, + 641EE6C42240C5CA00173FCB /* FBMacros.h in Headers */, + 641EE6C52240C5CA00173FCB /* XCTestExpectationDelegate-Protocol.h in Headers */, + 641EE6C62240C5CA00173FCB /* XCTUIApplicationMonitor-Protocol.h in Headers */, + 71822777258744CE00661B83 /* DDNumber.h in Headers */, + 641EE6C82240C5CA00173FCB /* XCTKVOExpectation.h in Headers */, + 641EE6C92240C5CA00173FCB /* XCUIDevice+FBRotation.h in Headers */, + 641EE6CA2240C5CA00173FCB /* XCEventGenerator.h in Headers */, + 719DCF162601EAFB000E765F /* FBNotificationsHelper.h in Headers */, + 71414ED52670A1EE003A8C5D /* LRUCache.h in Headers */, + 641EE6CB2240C5CA00173FCB /* FBConfiguration.h in Headers */, + 641EE6CC2240C5CA00173FCB /* XCTestSuiteRun.h in Headers */, + 641EE6CD2240C5CA00173FCB /* XCUIElementAsynchronousHandlerWrapper.h in Headers */, + 641EE6CE2240C5CA00173FCB /* XCTestLog.h in Headers */, + C845206222D5E79400EA68CB /* FBUnattachedAppLauncher.h in Headers */, + 641EE6CF2240C5CA00173FCB /* UITapGestureRecognizer-RecordingAdditions.h in Headers */, + 641EE6D02240C5CA00173FCB /* XCDebugLogDelegate-Protocol.h in Headers */, + 641EE6D12240C5CA00173FCB /* NSString-XCTAdditions.h in Headers */, + 641EE6D22240C5CA00173FCB /* XCTestWaiter.h in Headers */, + 641EE6D32240C5CA00173FCB /* FBImageUtils.h in Headers */, + 641EE6D42240C5CA00173FCB /* NSValue-XCTestAdditions.h in Headers */, + 641EE6D52240C5CA00173FCB /* _XCTWaiterImpl.h in Headers */, + 641EE6D62240C5CA00173FCB /* FBLogger.h in Headers */, + 71BB58F72B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */, + 641EE6D72240C5CA00173FCB /* XCTestObserver.h in Headers */, + 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */, + 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */, + 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, + 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */, + 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, + 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */, + 641EE6DE2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.h in Headers */, + 641EE6DF2240C5CA00173FCB /* FBMjpegServer.h in Headers */, + 641EE6E02240C5CA00173FCB /* XCUIRecorderNodeFinderMatch.h in Headers */, + 641EE6E12240C5CA00173FCB /* XCUIApplicationProcess.h in Headers */, + 641EE6E22240C5CA00173FCB /* FBW3CActionsSynthesizer.h in Headers */, + 641EE6E32240C5CA00173FCB /* CDStructures.h in Headers */, + 71822780258744D000661B83 /* DDRange.h in Headers */, + 641EE6E42240C5CA00173FCB /* XCKeyboardLayout.h in Headers */, + 641EE6E52240C5CA00173FCB /* XCTAsyncActivity-Protocol.h in Headers */, + 641EE6E62240C5CA00173FCB /* XCActivityRecord.h in Headers */, + 71822765258744C700661B83 /* HTTPDataResponse.h in Headers */, + 641EE6E72240C5CA00173FCB /* XCUIElement+FBFind.h in Headers */, + 641EE6E82240C5CA00173FCB /* XCTestManager_ManagerInterface-Protocol.h in Headers */, + 641EE6E92240C5CA00173FCB /* FBFailureProofTestCase.h in Headers */, + 641EE6EA2240C5CA00173FCB /* XCTTestRunSessionDelegate-Protocol.h in Headers */, + 641EE6EB2240C5CA00173FCB /* XCTestCaseSuite.h in Headers */, + 641EE6EC2240C5CA00173FCB /* _XCInternalTestRun.h in Headers */, + 641EE6ED2240C5CA00173FCB /* FBXPath-Private.h in Headers */, + 71D04DC925356C43008A052C /* XCUIElement+FBCaching.h in Headers */, + 641EE6EE2240C5CA00173FCB /* XCKeyMappingPath.h in Headers */, + 71C8E55225399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE158A961CBD452B00A3E3F0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + EEE376491D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h in Headers */, + 715AFAC11FFA29180053896D /* FBScreen.h in Headers */, + EE6B64FD1D0F86EF00E85F5D /* XCTestPrivateSymbols.h in Headers */, + AD76723D1D6B7CC000610457 /* XCUIElement+FBTyping.h in Headers */, + EEE376451D59F81400ED88DD /* XCUIElement+FBUtilities.h in Headers */, + EE158AB21CBD456F00A3E3F0 /* XCUIElement+FBScrolling.h in Headers */, + EE35AD361E3B77D600A02D78 /* XCSourceCodeTreeNode.h in Headers */, + EE35AD341E3B77D600A02D78 /* XCPointerEventPath.h in Headers */, + 713AE575243A53BE0000D657 /* FBW3CActionsHelpers.h in Headers */, + EE158AE11CBD456F00A3E3F0 /* FBRouteRequest.h in Headers */, + 71F5BE4F252F14EB00EE9EBA /* FBExceptions.h in Headers */, + 648C10AB22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */, + EE35AD401E3B77D600A02D78 /* XCTest.h in Headers */, + 716C9E0027315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h in Headers */, + 719CD8F82126C78F00C7D0C2 /* FBAlertsMonitor.h in Headers */, + EE158AE41CBD456F00A3E3F0 /* FBSession.h in Headers */, + 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, + EE35AD0F1E3B77D600A02D78 /* _XCTestImplementation.h in Headers */, + 71241D7B1FAE3D2500B9559F /* FBTouchActionCommands.h in Headers */, + EE158ACA1CBD456F00A3E3F0 /* FBTouchIDCommands.h in Headers */, + EE35AD6A1E3B77D600A02D78 /* XCUIApplication.h in Headers */, + EE158ABA1CBD456F00A3E3F0 /* FBCustomCommands.h in Headers */, + EE35AD0D1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h in Headers */, + EE158AC41CBD456F00A3E3F0 /* FBOrientationCommands.h in Headers */, + 71BB58EF2B96511800CB9BFE /* FBVideoCommands.h in Headers */, + 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */, + EE35AD611E3B77D600A02D78 /* XCTRunnerIDESession.h in Headers */, + EE158AE01CBD456F00A3E3F0 /* FBRouteRequest-Private.h in Headers */, + EE35AD621E3B77D600A02D78 /* XCTTestRunSession.h in Headers */, + EE35AD541E3B77D600A02D78 /* XCTestProbe.h in Headers */, + EE35AD281E3B77D600A02D78 /* XCApplicationQuery.h in Headers */, + E444DCB124913C220060D7EB /* RoutingConnection.h in Headers */, + EE35AD3C1E3B77D600A02D78 /* XCTAsyncActivity.h in Headers */, + EE35AD501E3B77D600A02D78 /* XCTestMisuseObserver.h in Headers */, + EE35AD601E3B77D600A02D78 /* XCTRunnerDaemonSession.h in Headers */, + 71414ED62670A1EE003A8C5D /* LRUCacheNode.h in Headers */, + 64B2650A228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, + 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */, + EE35AD4B1E3B77D600A02D78 /* XCTestExpectationWaiter.h in Headers */, + EE35AD1E1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h in Headers */, + 6496A5D9230D6EB30087F8CB /* AXSettings.h in Headers */, + EE35AD301E3B77D600A02D78 /* XCKeyboardKeyMap.h in Headers */, + EE35AD5D1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h in Headers */, + 714E14B829805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, + EE158B5F1CBD47A000A3E3F0 /* WebDriverAgentLib.h in Headers */, + EE158AC01CBD456F00A3E3F0 /* FBFindElementCommands.h in Headers */, + 71D475C22538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h in Headers */, + EE35AD551E3B77D600A02D78 /* XCTestRun.h in Headers */, + EE158AE61CBD456F00A3E3F0 /* FBWebServer.h in Headers */, + EE158AC61CBD456F00A3E3F0 /* FBScreenshotCommands.h in Headers */, + EE35AD0A1E3B77D600A02D78 /* _XCKVOExpectationImplementation.h in Headers */, + EE0D1F611EBCDCF7006A3123 /* NSString+FBVisualLength.h in Headers */, + EE35AD7B1E3B80C000A02D78 /* FBXCTestDaemonsProxy.h in Headers */, + EE35AD721E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h in Headers */, + EE35AD3F1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h in Headers */, + EE35AD5F1E3B77D600A02D78 /* XCTRunnerAutomationSession.h in Headers */, + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, + 71C9EAAC25E8415A00470CD8 /* FBScreenshot.h in Headers */, + EE35AD371E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h in Headers */, + EE158AB01CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.h in Headers */, + 71414ED42670A1EE003A8C5D /* LRUCache.h in Headers */, + EE158ADC1CBD456F00A3E3F0 /* FBResponsePayload.h in Headers */, + 13815F6F2328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */, + EE158ACC1CBD456F00A3E3F0 /* FBUnknownCommands.h in Headers */, + 641EE7052240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, + 71A224E51DE2F56600844D55 /* NSPredicate+FBFormat.h in Headers */, + EE35AD1F1E3B77D600A02D78 /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */, + EE35AD411E3B77D600A02D78 /* XCTestCase.h in Headers */, + EE35AD391E3B77D600A02D78 /* XCSymbolicatorHolder.h in Headers */, + EE35AD6B1E3B77D600A02D78 /* XCUIApplicationImpl.h in Headers */, + EE35AD201E3B77D600A02D78 /* UIPanGestureRecognizer-RecordingAdditions.h in Headers */, + 71555A3D1DEC460A007D4A8B /* NSExpression+FBFormat.h in Headers */, + 71D3B3D5267FC7260076473D /* XCUIElement+FBResolve.h in Headers */, + EE35AD0C1E3B77D600A02D78 /* _XCTestCaseImplementation.h in Headers */, + EE35AD211E3B77D600A02D78 /* UIPinchGestureRecognizer-RecordingAdditions.h in Headers */, + EE35AD4F1E3B77D600A02D78 /* XCTestManager_TestsInterface-Protocol.h in Headers */, + 719CD8FC2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h in Headers */, + EE18883A1DA661C400307AA8 /* FBMathUtils.h in Headers */, + 13DE7A49287C4005003243C6 /* FBXCDeviceEvent.h in Headers */, + 71B155DA23070ECF00646AFB /* FBHTTPStatusCodes.h in Headers */, + EE35AD221E3B77D600A02D78 /* UISwipeGestureRecognizer-RecordingAdditions.h in Headers */, + 713C6DCF1DDC772A00285B92 /* FBElementUtils.h in Headers */, + EE158ABC1CBD456F00A3E3F0 /* FBDebugCommands.h in Headers */, + EE35AD561E3B77D600A02D78 /* XCTestSuite.h in Headers */, + EE35AD6D1E3B77D600A02D78 /* XCUICoordinate.h in Headers */, + 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */, + EE35AD5C1E3B77D600A02D78 /* XCTNSPredicateExpectation.h in Headers */, + EE35AD521E3B77D600A02D78 /* XCTestObservationCenter.h in Headers */, + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, + EE35AD5B1E3B77D600A02D78 /* XCTNSNotificationExpectation.h in Headers */, + E444DC97249131D40060D7EB /* HTTPServer.h in Headers */, + E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */, + EE35AD751E3B77D600A02D78 /* XCUIRecorderNodeFinder.h in Headers */, + 1357E296233D05240054BDB8 /* XCUIHitPointResult.h in Headers */, + 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, + EE158AAE1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.h in Headers */, + EE35AD781E3B77D600A02D78 /* XCUIRecorderUtilities.h in Headers */, + EE35AD421E3B77D600A02D78 /* XCTestCaseRun.h in Headers */, + EE35AD441E3B77D600A02D78 /* XCTestConfiguration.h in Headers */, + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */, + EE35AD0B1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h in Headers */, + 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, + EE35AD491E3B77D600A02D78 /* XCTestExpectation.h in Headers */, + EE158AE81CBD456F00A3E3F0 /* FBElementTypeTransformer.h in Headers */, + 7157B291221DADD2001C348C /* FBXCAXClientProxy.h in Headers */, + EE158AD21CBD456F00A3E3F0 /* FBElementCache.h in Headers */, + EE35AD5A1E3B77D600A02D78 /* XCTMetric.h in Headers */, + EE35AD461E3B77D600A02D78 /* XCTestContextScope.h in Headers */, + 71BB58F62B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */, + 71A7EAF51E20516B001DA4F2 /* XCUIElement+FBClassChain.h in Headers */, + EE158ADA1CBD456F00A3E3F0 /* FBResponseJSONPayload.h in Headers */, + EE35AD3D1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h in Headers */, + EE158AD01CBD456F00A3E3F0 /* FBElement.h in Headers */, + EE35AD3E1E3B77D600A02D78 /* XCTAXClient-Protocol.h in Headers */, + EE158AD41CBD456F00A3E3F0 /* FBExceptionHandler.h in Headers */, + EE158ADE1CBD456F00A3E3F0 /* FBRoute.h in Headers */, + E444DC81249131B10060D7EB /* DDRange.h in Headers */, + EE35AD471E3B77D600A02D78 /* XCTestDriver.h in Headers */, + EE35AD121E3B77D600A02D78 /* _XCTNSNotificationExpectationImplementation.h in Headers */, + E444DC93249131D40060D7EB /* HTTPMessage.h in Headers */, + EE35AD3A1E3B77D600A02D78 /* XCSynthesizedEventRecord.h in Headers */, + E444DCAD24913C220060D7EB /* RouteResponse.h in Headers */, + EE35AD671E3B77D600A02D78 /* XCTWaiterDelegatePrivate-Protocol.h in Headers */, + EE35AD4D1E3B77D600A02D78 /* XCTestManager_IDEInterface-Protocol.h in Headers */, + 13DE7A5B287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */, + 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */, + EE35AD771E3B77D600A02D78 /* XCUIRecorderTimingMessage.h in Headers */, + E444DC83249131B10060D7EB /* DDNumber.h in Headers */, + EE35AD271E3B77D600A02D78 /* XCApplicationMonitor.h in Headers */, + EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */, + 71A5C67329A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */, + 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, + EE158AEA1CBD456F00A3E3F0 /* FBRuntimeUtils.h in Headers */, + 7136A4791E8918E60024FC3D /* XCUIElement+FBPickerWheel.h in Headers */, + E444DCB324913C220060D7EB /* RoutingHTTPServer.h in Headers */, + EE35AD511E3B77D600A02D78 /* XCTestObservation-Protocol.h in Headers */, + EE35AD131E3B77D600A02D78 /* _XCTNSPredicateExpectationImplementation.h in Headers */, + EE158ABE1CBD456F00A3E3F0 /* FBElementCommands.h in Headers */, + 715557D3211DBCE700613B26 /* FBTCPSocket.h in Headers */, + 71B49EC71ED1A58100D51AD6 /* XCUIElement+FBUID.h in Headers */, + EE35AD381E3B77D600A02D78 /* XCSymbolicationRecord.h in Headers */, + EE35AD6E1E3B77D600A02D78 /* XCUIDevice.h in Headers */, + 71BD20731F86116100B36EC2 /* XCUIApplication+FBTouchAction.h in Headers */, + EE158ACE1CBD456F00A3E3F0 /* FBCommandHandler.h in Headers */, + EE158AC81CBD456F00A3E3F0 /* FBSessionCommands.h in Headers */, + 71C8E55125399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */, + 641EE70B2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, + 63CCF91221ECE4C700E94ABD /* FBImageProcessor.h in Headers */, + EE158AE31CBD456F00A3E3F0 /* FBSession-Private.h in Headers */, + 716E0BCE1E917E810087A825 /* NSString+FBXMLSafeString.h in Headers */, + EE158ACF1CBD456F00A3E3F0 /* FBCommandStatus.h in Headers */, + EE158AB81CBD456F00A3E3F0 /* FBAlertViewCommands.h in Headers */, + EE35AD651E3B77D600A02D78 /* XCTWaiter.h in Headers */, + EE35AD681E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h in Headers */, + EE35AD451E3B77D600A02D78 /* XCTestContext.h in Headers */, + EE35AD661E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h in Headers */, + EE35AD0E1E3B77D600A02D78 /* _XCTestExpectationImplementation.h in Headers */, + EE35AD291E3B77D600A02D78 /* XCAXClient_iOS.h in Headers */, + EE35AD691E3B77D600A02D78 /* XCTWaiterManager.h in Headers */, + EE35AD481E3B77D600A02D78 /* XCTestDriverInterface-Protocol.h in Headers */, + 648C10AF22AAAE4000B81B9A /* TIPreferencesController.h in Headers */, + E444DC6C249131890060D7EB /* HTTPDataResponse.h in Headers */, + E444DC65249131890060D7EB /* HTTPErrorResponse.h in Headers */, + EE35AD111E3B77D600A02D78 /* _XCTestSuiteImplementation.h in Headers */, + 714097431FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h in Headers */, + AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */, + EE35AD731E3B77D600A02D78 /* XCUIElementQuery.h in Headers */, + EE35AD331E3B77D600A02D78 /* XCPointerEvent.h in Headers */, + EE35AD351E3B77D600A02D78 /* XCSourceCodeRecording.h in Headers */, + 71D04DC825356C43008A052C /* XCUIElement+FBCaching.h in Headers */, + 71BB58E12B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */, + E444DC99249131D40060D7EB /* HTTPLogging.h in Headers */, + E444DC9B249131D40060D7EB /* HTTPResponse.h in Headers */, + EEE9B4721CD02B88009D2030 /* FBRunLoopSpinner.h in Headers */, + EE3A18621CDE618F00DE4205 /* FBErrorBuilder.h in Headers */, + EE35AD261E3B77D600A02D78 /* XCApplicationMonitor_iOS.h in Headers */, + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, + EE3A18661CDE734B00DE4205 /* FBKeyboard.h in Headers */, + AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */, + 714D88CC2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */, + EE35AD101E3B77D600A02D78 /* _XCTestObservationCenterImplementation.h in Headers */, + AD6C26981CF2481700F8B5FF /* XCUIDevice+FBHelpers.h in Headers */, + 71A7EAF91E224648001DA4F2 /* FBClassChainQueryParser.h in Headers */, + EE9B76AA1CF7A43900275851 /* FBMacros.h in Headers */, + C8FB547922D4C1FC00B69954 /* FBUnattachedAppLauncher.h in Headers */, + 719DCF152601EAFB000E765F /* FBNotificationsHelper.h in Headers */, + EE35AD4A1E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h in Headers */, + 71F3E7D425417FF400E0C22B /* FBSettings.h in Headers */, + EE35AD641E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h in Headers */, + EE35AD591E3B77D600A02D78 /* XCTKVOExpectation.h in Headers */, + 13DE7A43287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */, + EEE376431D59F81400ED88DD /* XCUIDevice+FBRotation.h in Headers */, + EE35AD2E1E3B77D600A02D78 /* XCEventGenerator.h in Headers */, + EE9B76A61CF7A43900275851 /* FBConfiguration.h in Headers */, + EE35AD571E3B77D600A02D78 /* XCTestSuiteRun.h in Headers */, + EE35AD701E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h in Headers */, + EE35AD4C1E3B77D600A02D78 /* XCTestLog.h in Headers */, + B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, + 71BB58E82B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */, + EE35AD231E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h in Headers */, + EE35AD2A1E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h in Headers */, + EE35AD1C1E3B77D600A02D78 /* NSString-XCTAdditions.h in Headers */, + EE35AD581E3B77D600A02D78 /* XCTestWaiter.h in Headers */, + 7150348721A6DAD600A0F4BA /* FBImageUtils.h in Headers */, + C8FB547422D3949C00B69954 /* LSApplicationWorkspace.h in Headers */, + E444DCAF24913C220060D7EB /* Route.h in Headers */, + EE35AD1D1E3B77D600A02D78 /* NSValue-XCTestAdditions.h in Headers */, + EE35AD141E3B77D600A02D78 /* _XCTWaiterImpl.h in Headers */, + EE9B76A81CF7A43900275851 /* FBLogger.h in Headers */, + EE35AD531E3B77D600A02D78 /* XCTestObserver.h in Headers */, + EE35AD6F1E3B77D600A02D78 /* XCUIElement.h in Headers */, + EE35AD2F1E3B77D600A02D78 /* XCKeyboardInputSolver.h in Headers */, + 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */, + EE7E271C1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h in Headers */, + EEDFE1211D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h in Headers */, + 7155D703211DCEF400166C20 /* FBMjpegServer.h in Headers */, + EE35AD761E3B77D600A02D78 /* XCUIRecorderNodeFinderMatch.h in Headers */, + EE35AD6C1E3B77D600A02D78 /* XCUIApplicationProcess.h in Headers */, + 7140974B1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h in Headers */, + EE35AD151E3B77D600A02D78 /* CDStructures.h in Headers */, + 71E75E6D254824230099FC87 /* XCUIElementQuery+FBHelpers.h in Headers */, + EE35AD311E3B77D600A02D78 /* XCKeyboardLayout.h in Headers */, + 716C9DFA27315D21005AD475 /* FBReflectionUtils.h in Headers */, + E444DCB624913C220060D7EB /* RouteRequest.h in Headers */, + 71F5BE23252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */, + 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */, + EE35AD3B1E3B77D600A02D78 /* XCTAsyncActivity-Protocol.h in Headers */, + EE35AD251E3B77D600A02D78 /* XCActivityRecord.h in Headers */, + EEBBD48B1D47746D00656A81 /* XCUIElement+FBFind.h in Headers */, + EE35AD4E1E3B77D600A02D78 /* XCTestManager_ManagerInterface-Protocol.h in Headers */, + EE6A893A1D0B38640083E92B /* FBFailureProofTestCase.h in Headers */, + E444DC95249131D40060D7EB /* HTTPConnection.h in Headers */, + EE35AD631E3B77D600A02D78 /* XCTTestRunSessionDelegate-Protocol.h in Headers */, + EE35AD431E3B77D600A02D78 /* XCTestCaseSuite.h in Headers */, + EE35AD091E3B77D600A02D78 /* _XCInternalTestRun.h in Headers */, + 712A0C871DA3E55D007D02E5 /* FBXPath-Private.h in Headers */, + EE35AD321E3B77D600A02D78 /* XCKeyMappingPath.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 641EE2D92240BBE300173FCB /* WebDriverAgentRunner_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 641EE2DF2240BBE300173FCB /* Build configuration list for PBXNativeTarget "WebDriverAgentRunner_tvOS" */; + buildPhases = ( + 641EE2D62240BBE300173FCB /* Sources */, + 641EE2D72240BBE300173FCB /* Frameworks */, + 641EE2D82240BBE300173FCB /* Resources */, + 641EE3472240C1EF00173FCB /* Copy frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 641EE6FB2240C5F400173FCB /* PBXTargetDependency */, + ); + name = WebDriverAgentRunner_tvOS; + productName = WebDriverAgent; + productReference = 641EE2DA2240BBE300173FCB /* WebDriverAgentRunner_tvOS.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 641EE5D52240C5CA00173FCB /* WebDriverAgentLib_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 641EE6F52240C5CA00173FCB /* Build configuration list for PBXNativeTarget "WebDriverAgentLib_tvOS" */; + buildPhases = ( + 641EE5D62240C5CA00173FCB /* Sources */, + 641EE6282240C5CA00173FCB /* Frameworks */, + 641EE6302240C5CA00173FCB /* Headers */, + 641EE6EF2240C5CA00173FCB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WebDriverAgentLib_tvOS; + productName = WebDriverAgentLib_; + productReference = 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */; + productType = "com.apple.product-type.framework"; + }; + 64B264F8228C50E0002A5025 /* UnitTests_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 64B26501228C50E0002A5025 /* Build configuration list for PBXNativeTarget "UnitTests_tvOS" */; + buildPhases = ( + 64B264F5228C50E0002A5025 /* Sources */, + 64B264F6228C50E0002A5025 /* Frameworks */, + 64B264F7228C50E0002A5025 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 64B26500228C50E0002A5025 /* PBXTargetDependency */, + ); + name = UnitTests_tvOS; + productName = WebDriverAgentLib_tvOSTests; + productReference = 64B264F9228C50E0002A5025 /* UnitTests_tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE158AA01CBD452B00A3E3F0 /* Build configuration list for PBXNativeTarget "WebDriverAgentLib" */; + buildPhases = ( + EE158A941CBD452B00A3E3F0 /* Sources */, + EE158A951CBD452B00A3E3F0 /* Frameworks */, + EE158A961CBD452B00A3E3F0 /* Headers */, + EE158A971CBD452B00A3E3F0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WebDriverAgentLib; + productName = WebDriverAgentLib_; + productReference = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; + productType = "com.apple.product-type.framework"; + }; + EE2202031ECC612200A29571 /* IntegrationTests_3 */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE2202191ECC612200A29571 /* Build configuration list for PBXNativeTarget "IntegrationTests_3" */; + buildPhases = ( + EE2202081ECC612200A29571 /* Sources */, + EE2202151ECC612200A29571 /* Frameworks */, + EE2202181ECC612200A29571 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EE2202041ECC612200A29571 /* PBXTargetDependency */, + EE2202061ECC612200A29571 /* PBXTargetDependency */, + ); + name = IntegrationTests_3; + productName = EvalUITests; + productReference = EE22021C1ECC612200A29571 /* IntegrationTests_3.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + EE5095DD1EBCC9090028E2FE /* IntegrationTests_2 */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE5095FB1EBCC9090028E2FE /* Build configuration list for PBXNativeTarget "IntegrationTests_2" */; + buildPhases = ( + EE5095E21EBCC9090028E2FE /* Sources */, + EE5095F71EBCC9090028E2FE /* Frameworks */, + EE5095FA1EBCC9090028E2FE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EE5095DE1EBCC9090028E2FE /* PBXTargetDependency */, + EE5095E01EBCC9090028E2FE /* PBXTargetDependency */, + ); + name = IntegrationTests_2; + productName = EvalUITests; + productReference = EE5095FE1EBCC9090028E2FE /* IntegrationTests_2.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + EE836C011C0F118600D87246 /* UnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE836C0A1C0F118600D87246 /* Build configuration list for PBXNativeTarget "UnitTests" */; + buildPhases = ( + EE836BFE1C0F118600D87246 /* Sources */, + EE836BFF1C0F118600D87246 /* Frameworks */, + EE836C001C0F118600D87246 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AD8D96F11D3C12960061268E /* PBXTargetDependency */, + ); + name = UnitTests; + productName = WebDriverAgentCoreTests; + productReference = EE836C021C0F118600D87246 /* UnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EE9B75D31CF7956C00275851 /* IntegrationApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE9B75F71CF7956C00275851 /* Build configuration list for PBXNativeTarget "IntegrationApp" */; + buildPhases = ( + EE9B75D01CF7956C00275851 /* Sources */, + EE9B75D11CF7956C00275851 /* Frameworks */, + EE9B75D21CF7956C00275851 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IntegrationApp; + productName = Eval; + productReference = EE9B75D41CF7956C00275851 /* IntegrationApp.app */; + productType = "com.apple.product-type.application"; + }; + EE9B75EB1CF7956C00275851 /* IntegrationTests_1 */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE9B75F81CF7956C00275851 /* Build configuration list for PBXNativeTarget "IntegrationTests_1" */; + buildPhases = ( + EE9B75E81CF7956C00275851 /* Sources */, + EE9B75E91CF7956C00275851 /* Frameworks */, + EE9B75EA1CF7956C00275851 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EE9B769F1CF79C0A00275851 /* PBXTargetDependency */, + EE9B75EE1CF7956C00275851 /* PBXTargetDependency */, + ); + name = IntegrationTests_1; + productName = EvalUITests; + productReference = EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + EEF988291C486603005CA669 /* WebDriverAgentRunner */ = { + isa = PBXNativeTarget; + buildConfigurationList = EEF988311C486604005CA669 /* Build configuration list for PBXNativeTarget "WebDriverAgentRunner" */; + buildPhases = ( + EEF988261C486603005CA669 /* Sources */, + EEF988271C486603005CA669 /* Frameworks */, + EEF988281C486603005CA669 /* Resources */, + EE93CFF41CCA501300708122 /* Copy frameworks */, + ); + buildRules = ( + ); + dependencies = ( + EE158B5C1CBD462500A3E3F0 /* PBXTargetDependency */, + ); + name = WebDriverAgentRunner; + productName = XCTUITestRunner; + productReference = EEF9882A1C486603005CA669 /* yolo.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 91F9DAE11B99DBC2001349B2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1310; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 641EE2D92240BBE300173FCB = { + CreatedOnToolsVersion = 10.1; + }; + 641EE5D52240C5CA00173FCB = { + ProvisioningStyle = Manual; + }; + 64B264F8228C50E0002A5025 = { + CreatedOnToolsVersion = 10.2.1; + }; + EE158A981CBD452B00A3E3F0 = { + CreatedOnToolsVersion = 7.3; + }; + EE836C011C0F118600D87246 = { + CreatedOnToolsVersion = 7.1.1; + }; + EE9B75D31CF7956C00275851 = { + CreatedOnToolsVersion = 7.3.1; + }; + EE9B75EB1CF7956C00275851 = { + CreatedOnToolsVersion = 7.3.1; + TestTargetID = EE9B75D31CF7956C00275851; + }; + EEF988291C486603005CA669 = { + CreatedOnToolsVersion = 7.2; + }; + }; + }; + buildConfigurationList = 91F9DAE41B99DBC2001349B2 /* Build configuration list for PBXProject "WebDriverAgent" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 91F9DAE01B99DBC2001349B2; + productRefGroup = 91F9DAEA1B99DBC2001349B2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */, + 641EE5D52240C5CA00173FCB /* WebDriverAgentLib_tvOS */, + EEF988291C486603005CA669 /* WebDriverAgentRunner */, + 641EE2D92240BBE300173FCB /* WebDriverAgentRunner_tvOS */, + EE836C011C0F118600D87246 /* UnitTests */, + 64B264F8228C50E0002A5025 /* UnitTests_tvOS */, + EE9B75EB1CF7956C00275851 /* IntegrationTests_1 */, + EE5095DD1EBCC9090028E2FE /* IntegrationTests_2 */, + EE2202031ECC612200A29571 /* IntegrationTests_3 */, + EE9B75D31CF7956C00275851 /* IntegrationApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 641EE2D82240BBE300173FCB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 641EE6EF2240C5CA00173FCB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 64B264F7228C50E0002A5025 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE158A971CBD452B00A3E3F0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE2202181ECC612200A29571 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE5095FA1EBCC9090028E2FE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE836C001C0F118600D87246 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75D21CF7956C00275851 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9B76941CF7997600275851 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75EA1CF7956C00275851 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EEF988281C486603005CA669 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 641EE2D62240BBE300173FCB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 641EE3452240C1C800173FCB /* UITestingUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 641EE5D62240C5CA00173FCB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, + 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */, + E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */, + 71D3B3D8267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, + E444DCBE24917A5E0060D7EB /* Route.m in Sources */, + E444DCC024917A5E0060D7EB /* RouteRequest.m in Sources */, + 13DE7A52287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */, + E444DCC224917A5E0060D7EB /* RouteResponse.m in Sources */, + E444DCC424917A5E0060D7EB /* RoutingConnection.m in Sources */, + E444DCC624917A5E0060D7EB /* RoutingHTTPServer.m in Sources */, + E444DCC824917A5E0060D7EB /* HTTPConnection.m in Sources */, + E444DCCB24917A5E0060D7EB /* HTTPMessage.m in Sources */, + E444DCCE24917A5E0060D7EB /* HTTPServer.m in Sources */, + E444DCD024917A5E0060D7EB /* HTTPDataResponse.m in Sources */, + E444DCD224917A5E0060D7EB /* HTTPErrorResponse.m in Sources */, + 71414ED92670A1EE003A8C5D /* LRUCache.m in Sources */, + E444DCD424917A5E0060D7EB /* DDNumber.m in Sources */, + E444DCD624917A5E0060D7EB /* DDRange.m in Sources */, + 641EE5D72240C5CA00173FCB /* FBScreenshotCommands.m in Sources */, + 71F3E7D725417FF400E0C22B /* FBSettings.m in Sources */, + 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */, + 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */, + 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */, + 71C8E55425399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */, + 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */, + 641EE70F2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, + 71BB58EB2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, + 714D88CF2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, + 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */, + 714E14BB29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, + 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */, + 641EE5E02240C5CA00173FCB /* FBTCPSocket.m in Sources */, + 641EE5E12240C5CA00173FCB /* FBErrorBuilder.m in Sources */, + 71C9EAAF25E8415A00470CD8 /* FBScreenshot.m in Sources */, + 641EE5E22240C5CA00173FCB /* XCUIElement+FBClassChain.m in Sources */, + 13DE7A4C287C4005003243C6 /* FBXCDeviceEvent.m in Sources */, + 641EE5E32240C5CA00173FCB /* NSExpression+FBFormat.m in Sources */, + 641EE5E42240C5CA00173FCB /* XCUIApplication+FBHelpers.m in Sources */, + 641EE5E52240C5CA00173FCB /* FBKeyboard.m in Sources */, + 641EE5E62240C5CA00173FCB /* FBElementUtils.m in Sources */, + 641EE5E72240C5CA00173FCB /* FBW3CActionsSynthesizer.m in Sources */, + 641EE5E92240C5CA00173FCB /* FBFailureProofTestCase.m in Sources */, + 641EE5EA2240C5CA00173FCB /* XCUIElement+FBIsVisible.m in Sources */, + 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */, + 641EE5EB2240C5CA00173FCB /* XCUIElement+FBFind.m in Sources */, + 641EE5EC2240C5CA00173FCB /* FBResponsePayload.m in Sources */, + C845206322D5E79700EA68CB /* FBUnattachedAppLauncher.m in Sources */, + 641EE5ED2240C5CA00173FCB /* FBRoute.m in Sources */, + 641EE5EE2240C5CA00173FCB /* NSString+FBVisualLength.m in Sources */, + 641EE5EF2240C5CA00173FCB /* FBRunLoopSpinner.m in Sources */, + 641EE5F02240C5CA00173FCB /* FBAlertsMonitor.m in Sources */, + 641EE5F12240C5CA00173FCB /* FBClassChainQueryParser.m in Sources */, + 641EE5F22240C5CA00173FCB /* NSPredicate+FBFormat.m in Sources */, + 718F49CA23087AD30045FE8B /* FBProtocolHelpers.m in Sources */, + 641EE5F42240C5CA00173FCB /* XCUIDevice+FBRotation.m in Sources */, + 13815F722328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */, + 71D475C52538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, + 641EE5F52240C5CA00173FCB /* XCUIElement+FBUID.m in Sources */, + 641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */, + 641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */, + 718226D12587443700661B83 /* GCDAsyncUdpSocket.m in Sources */, + 641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */, + 641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */, + 641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */, + 13DE7A46287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */, + 641EE5FE2240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.m in Sources */, + 641EE5FF2240C5CA00173FCB /* XCUIElement+FBForceTouch.m in Sources */, + 716C9E0327315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m in Sources */, + 641EE6002240C5CA00173FCB /* FBTouchActionCommands.m in Sources */, + 719DCF182601EAFB000E765F /* FBNotificationsHelper.m in Sources */, + 714EAA102673FDFE005C5B47 /* FBCapabilities.m in Sources */, + 641EE6012240C5CA00173FCB /* FBImageProcessor.m in Sources */, + 641EE6022240C5CA00173FCB /* FBTouchIDCommands.m in Sources */, + 641EE6032240C5CA00173FCB /* FBDebugCommands.m in Sources */, + 641EE6042240C5CA00173FCB /* NSString+FBXMLSafeString.m in Sources */, + 641EE6052240C5CA00173FCB /* FBUnknownCommands.m in Sources */, + 641EE6062240C5CA00173FCB /* FBOrientationCommands.m in Sources */, + 641EE7092240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */, + 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */, + 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */, + 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */, + B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, + 641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */, + 71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, + 641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */, + 641EE60D2240C5CA00173FCB /* XCTestPrivateSymbols.m in Sources */, + 641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */, + 641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */, + 641EE6102240C5CA00173FCB /* FBImageUtils.m in Sources */, + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */, + 641EE6112240C5CA00173FCB /* FBSession.m in Sources */, + 641EE6122240C5CA00173FCB /* FBFindElementCommands.m in Sources */, + 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, + 641EE6132240C5CA00173FCB /* FBDebugLogDelegateDecorator.m in Sources */, + 641EE6142240C5CA00173FCB /* FBAlertViewCommands.m in Sources */, + 71414EDB2670A1EE003A8C5D /* LRUCacheNode.m in Sources */, + 71BB58F92B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, + 641EE6152240C5CA00173FCB /* XCUIElement+FBScrolling.m in Sources */, + 641EE6162240C5CA00173FCB /* FBSessionCommands.m in Sources */, + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, + 641EE6192240C5CA00173FCB /* FBConfiguration.m in Sources */, + 641EE61A2240C5CA00173FCB /* FBElementCache.m in Sources */, + 71F5BE26252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */, + 641EE61B2240C5CA00173FCB /* FBPasteboard.m in Sources */, + 641EE61C2240C5CA00173FCB /* FBAlert.m in Sources */, + 718F49CB23087B040045FE8B /* FBCommandStatus.m in Sources */, + 716C9DFD27315D21005AD475 /* FBReflectionUtils.m in Sources */, + 641EE61D2240C5CA00173FCB /* FBElementCommands.m in Sources */, + 641EE61E2240C5CA00173FCB /* FBExceptionHandler.m in Sources */, + 71BB58F22B96511800CB9BFE /* FBVideoCommands.m in Sources */, + 641EE61F2240C5CA00173FCB /* FBXCodeCompatibility.m in Sources */, + 71E75E70254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */, + 641EE6212240C5CA00173FCB /* FBElementTypeTransformer.m in Sources */, + 13DE7A5E287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m in Sources */, + 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */, + 71D04DCB25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, + 641EE6242240C5CA00173FCB /* FBXCTestDaemonsProxy.m in Sources */, + 13DE7A58287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */, + 641EE6262240C5CA00173FCB /* FBMathUtils.m in Sources */, + 641EE6272240C5CA00173FCB /* FBXCAXClientProxy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 64B264F5228C50E0002A5025 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64B26508228C5514002A5025 /* XCUIElementDouble.m in Sources */, + 64B26504228C5299002A5025 /* FBTVNavigationTrackerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE158A941CBD452B00A3E3F0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE158AC71CBD456F00A3E3F0 /* FBScreenshotCommands.m in Sources */, + E444DC98249131D40060D7EB /* HTTPConnection.m in Sources */, + 7136A47A1E8918E60024FC3D /* XCUIElement+FBPickerWheel.m in Sources */, + E444DC84249131B10060D7EB /* DDRange.m in Sources */, + 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */, + 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, + 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */, + 719CD8FD2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m in Sources */, + 13DE7A45287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */, + 641EE70E2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, + 71BD20741F86116100B36EC2 /* XCUIApplication+FBTouchAction.m in Sources */, + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, + EE158AE71CBD456F00A3E3F0 /* FBWebServer.m in Sources */, + 715557D4211DBCE700613B26 /* FBTCPSocket.m in Sources */, + EE3A18631CDE618F00DE4205 /* FBErrorBuilder.m in Sources */, + 71A7EAF61E20516B001DA4F2 /* XCUIElement+FBClassChain.m in Sources */, + 71555A3E1DEC460A007D4A8B /* NSExpression+FBFormat.m in Sources */, + AD6C269D1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m in Sources */, + EE3A18671CDE734B00DE4205 /* FBKeyboard.m in Sources */, + 719DCF172601EAFB000E765F /* FBNotificationsHelper.m in Sources */, + E444DCAC24913C220060D7EB /* Route.m in Sources */, + 713C6DD01DDC772A00285B92 /* FBElementUtils.m in Sources */, + 71BB58E32B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, + 7140974C1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m in Sources */, + EE6A893B1D0B38640083E92B /* FBFailureProofTestCase.m in Sources */, + 713AE576243A53BE0000D657 /* FBW3CActionsHelpers.m in Sources */, + 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */, + EE158AB11CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.m in Sources */, + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, + EEBBD48C1D47746D00656A81 /* XCUIElement+FBFind.m in Sources */, + EE158ADD1CBD456F00A3E3F0 /* FBResponsePayload.m in Sources */, + B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, + E444DCB524913C220060D7EB /* RouteRequest.m in Sources */, + C8FB547A22D4C1FC00B69954 /* FBUnattachedAppLauncher.m in Sources */, + EE158ADF1CBD456F00A3E3F0 /* FBRoute.m in Sources */, + EE0D1F621EBCDCF7006A3123 /* NSString+FBVisualLength.m in Sources */, + EEE9B4731CD02B88009D2030 /* FBRunLoopSpinner.m in Sources */, + 719CD8F92126C78F00C7D0C2 /* FBAlertsMonitor.m in Sources */, + 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */, + 718226D02587443700661B83 /* GCDAsyncUdpSocket.m in Sources */, + 13DE7A51287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */, + 71A224E61DE2F56600844D55 /* NSPredicate+FBFormat.m in Sources */, + E444DC85249131B10060D7EB /* DDNumber.m in Sources */, + EEE376441D59F81400ED88DD /* XCUIDevice+FBRotation.m in Sources */, + 13815F712328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */, + 71B49EC81ED1A58100D51AD6 /* XCUIElement+FBUID.m in Sources */, + EE158AE21CBD456F00A3E3F0 /* FBRouteRequest.m in Sources */, + EE158ADB1CBD456F00A3E3F0 /* FBResponseJSONPayload.m in Sources */, + 714EAA0F2673FDFE005C5B47 /* FBCapabilities.m in Sources */, + 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */, + EEDFE1221D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m in Sources */, + 714D88CE2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, + E444DCB424913C220060D7EB /* RoutingHTTPServer.m in Sources */, + 7140974E1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m in Sources */, + EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */, + EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */, + 71241D7C1FAE3D2500B9559F /* FBTouchActionCommands.m in Sources */, + 63CCF91321ECE4C700E94ABD /* FBImageProcessor.m in Sources */, + EE158ACB1CBD456F00A3E3F0 /* FBTouchIDCommands.m in Sources */, + 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */, + EE158ABD1CBD456F00A3E3F0 /* FBDebugCommands.m in Sources */, + 716E0BCF1E917E810087A825 /* NSString+FBXMLSafeString.m in Sources */, + 71BB58EA2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, + EE158ACD1CBD456F00A3E3F0 /* FBUnknownCommands.m in Sources */, + EE158AC51CBD456F00A3E3F0 /* FBOrientationCommands.m in Sources */, + 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, + 71D475C42538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, + 641EE7082240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */, + 71E75E6F254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */, + 71D04DCA25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, + EE158AEB1CBD456F00A3E3F0 /* FBRuntimeUtils.m in Sources */, + EEE376461D59F81400ED88DD /* XCUIElement+FBUtilities.m in Sources */, + EE9B76A91CF7A43900275851 /* FBLogger.m in Sources */, + EE158ABB1CBD456F00A3E3F0 /* FBCustomCommands.m in Sources */, + AD6C26991CF2481700F8B5FF /* XCUIDevice+FBHelpers.m in Sources */, + 716C9E0227315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m in Sources */, + EE6B64FE1D0F86EF00E85F5D /* XCTestPrivateSymbols.m in Sources */, + AD76723E1D6B7CC000610457 /* XCUIElement+FBTyping.m in Sources */, + EE158AAF1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.m in Sources */, + 714E14BA29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, + 7150348821A6DAD600A0F4BA /* FBImageUtils.m in Sources */, + E444DCAB24913C220060D7EB /* HTTPResponseProxy.m in Sources */, + E444DC6D249131890060D7EB /* HTTPErrorResponse.m in Sources */, + 71F5BE25252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */, + EE158AE51CBD456F00A3E3F0 /* FBSession.m in Sources */, + 71C9EAAE25E8415A00470CD8 /* FBScreenshot.m in Sources */, + E444DCB224913C220060D7EB /* RoutingConnection.m in Sources */, + EE158AC11CBD456F00A3E3F0 /* FBFindElementCommands.m in Sources */, + EE7E271D1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m in Sources */, + 716C9DFC27315D21005AD475 /* FBReflectionUtils.m in Sources */, + 71C8E55325399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */, + 71414EDA2670A1EE003A8C5D /* LRUCacheNode.m in Sources */, + EE158AB91CBD456F00A3E3F0 /* FBAlertViewCommands.m in Sources */, + 71BB58F12B96511800CB9BFE /* FBVideoCommands.m in Sources */, + 71F3E7D625417FF400E0C22B /* FBSettings.m in Sources */, + 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */, + 71BB58F82B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, + EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */, + 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */, + EE158AC91CBD456F00A3E3F0 /* FBSessionCommands.m in Sources */, + 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */, + EE9B76A71CF7A43900275851 /* FBConfiguration.m in Sources */, + E444DC9C249131D40060D7EB /* HTTPServer.m in Sources */, + 71414ED82670A1EE003A8C5D /* LRUCache.m in Sources */, + E444DC67249131890060D7EB /* HTTPDataResponse.m in Sources */, + EE158AD31CBD456F00A3E3F0 /* FBElementCache.m in Sources */, + 71930C4320662E1F00D3AFEC /* FBPasteboard.m in Sources */, + AD6C26951CF2379700F8B5FF /* FBAlert.m in Sources */, + EE158ABF1CBD456F00A3E3F0 /* FBElementCommands.m in Sources */, + 13DE7A5D287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m in Sources */, + 13DE7A4B287C4005003243C6 /* FBXCDeviceEvent.m in Sources */, + EE158AD51CBD456F00A3E3F0 /* FBExceptionHandler.m in Sources */, + EE5A24421F136D360078B1D9 /* FBXCodeCompatibility.m in Sources */, + EE158AE91CBD456F00A3E3F0 /* FBElementTypeTransformer.m in Sources */, + E444DC9D249131D40060D7EB /* HTTPMessage.m in Sources */, + E444DCB024913C220060D7EB /* RouteResponse.m in Sources */, + 71D3B3D7267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, + 715AFAC21FFA29180053896D /* FBScreen.m in Sources */, + 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */, + EE35AD7C1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m in Sources */, + EE18883B1DA661C400307AA8 /* FBMathUtils.m in Sources */, + 7157B292221DADD2001C348C /* FBXCAXClientProxy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE2202081ECC612200A29571 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */, + 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */, + 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */, + 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, + 719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */, + EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */, + 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */, + 71BB58EC2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, + EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */, + 71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */, + 71BB58E52B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, + 71BB58FA2B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, + 7150FFF722476B3A00B2EE28 /* FBForceTouchTests.m in Sources */, + 71BB58F32B96511800CB9BFE /* FBVideoCommands.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE5095E21EBCC9090028E2FE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE5095E51EBCC9090028E2FE /* FBTypingTest.m in Sources */, + 63FD950321F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, + EE5095EB1EBCC9090028E2FE /* XCElementSnapshotHitPointTests.m in Sources */, + EE5095EC1EBCC9090028E2FE /* XCUIApplicationHelperTests.m in Sources */, + 7136C0F9243A182400921C76 /* FBW3CTypeActionsTests.m in Sources */, + EE5095ED1EBCC9090028E2FE /* XCElementSnapshotHelperTests.m in Sources */, + EE5095EE1EBCC9090028E2FE /* FBXPathIntegrationTests.m in Sources */, + EE5095EF1EBCC9090028E2FE /* XCUIElementHelperIntegrationTests.m in Sources */, + EE5095F01EBCC9090028E2FE /* XCUIDeviceHelperTests.m in Sources */, + 644D9CCE230E1F1A00C90459 /* FBConfigurationTests.m in Sources */, + EE5095F11EBCC9090028E2FE /* XCUIElementFBFindTests.m in Sources */, + EE5095F21EBCC9090028E2FE /* XCUIDeviceRotationTests.m in Sources */, + EE5095F41EBCC9090028E2FE /* XCUIDeviceHealthCheckTests.m in Sources */, + EE5096021EBCD0250028E2FE /* FBIntegrationTestCase.m in Sources */, + EE5095F51EBCC9090028E2FE /* XCUIElementAttributesTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE836BFE1C0F118600D87246 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 713352FD26CEF31D00523CBC /* FBLRUCacheTests.m in Sources */, + EE3F8CFE1D08AA17006F02CE /* FBRunLoopSpinnerTests.m in Sources */, + 714801D11FA9D9FA00DC5997 /* FBSDKVersionTests.m in Sources */, + EE3F8D001D08B05F006F02CE /* FBElementTypeTransformerTests.m in Sources */, + 13FFF2F2287DBEE600E561E4 /* XCElementSnapshotDouble.m in Sources */, + EEE16E971D33A25500172525 /* FBConfigurationTests.m in Sources */, + ADBC39941D0782CD00327304 /* FBElementCacheTests.m in Sources */, + 715D554B2229891B00524509 /* FBExceptionHandlerTests.m in Sources */, + 718F49C8230844330045FE8B /* FBProtocolHelpersTests.m in Sources */, + 719FF5B91DAD21F5008E0099 /* FBElementUtilitiesTests.m in Sources */, + 716E0BD11E917F260087A825 /* FBXMLSafeStringTests.m in Sources */, + ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */, + EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */, + 7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */, + 71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */, + EE6A892B1D0B25820083E92B /* XCUIApplicationDouble.m in Sources */, + 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */, + EE6A892D1D0B2AF40083E92B /* FBErrorBuilderTests.m in Sources */, + 712A0C851DA3E459007D02E5 /* FBXPathTests.m in Sources */, + ADBC39981D07842800327304 /* XCUIElementDouble.m in Sources */, + 7139145A1DF01989005896C2 /* XCUIElementHelpersTests.m in Sources */, + EE6A89261D0B19E60083E92B /* FBSessionTests.m in Sources */, + 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */, + EE18883D1DA663EB00307AA8 /* FBMathUtilsTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75D01CF7956C00275851 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */, + EE1E06E71D182E95007CF043 /* FBAlertViewController.m in Sources */, + 315A15072518CC2800A3A064 /* TouchSpotView.m in Sources */, + EE9B76911CF7997600275851 /* main.m in Sources */, + EE9B768F1CF7997600275851 /* ViewController.m in Sources */, + 315A15012518CB8700A3A064 /* TouchableView.m in Sources */, + 315A150A2518D6F400A3A064 /* TouchViewController.m in Sources */, + ADDA07241D6BB2BF001700AC /* FBScrollViewController.m in Sources */, + EE8BA97A1DCCED9A00A9DEF8 /* FBNavigationController.m in Sources */, + EE55B3251D1D5388003AAAEC /* FBTableDataSource.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE9B75E81CF7956C00275851 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE26409D1D0EBA25009BE6B0 /* FBElementAttributeTests.m in Sources */, + 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */, + 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */, + EE1E06DA1D1808C2007CF043 /* FBIntegrationTestCase.m in Sources */, + 63FD950421F9D06200A3E356 /* FBImageProcessorTests.m in Sources */, + EE05BAFA1D13003C00A3EB00 /* FBKeyboardTests.m in Sources */, + EE55B3271D1D54CF003AAAEC /* FBScrollingTests.m in Sources */, + EE6A89371D0B35920083E92B /* FBFailureProofTestCaseTests.m in Sources */, + EE006EAD1EB99B15006900A4 /* FBElementVisibilityTests.m in Sources */, + 71F5BE34252E5B2200EE9EBA /* FBElementSwipingTests.m in Sources */, + 7152EB301F41F9960047EEFF /* FBSessionIntegrationTests.m in Sources */, + EE9B769A1CF799F400275851 /* FBAlertTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EEF988261C486603005CA669 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9AB8011CAEE048008C271F /* UITestingUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 641EE6FB2240C5F400173FCB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 641EE5D52240C5CA00173FCB /* WebDriverAgentLib_tvOS */; + targetProxy = 641EE6FA2240C5F400173FCB /* PBXContainerItemProxy */; + }; + 64B26500228C50E0002A5025 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 641EE5D52240C5CA00173FCB /* WebDriverAgentLib_tvOS */; + targetProxy = 64B264FF228C50E0002A5025 /* PBXContainerItemProxy */; + }; + AD8D96F11D3C12960061268E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; + targetProxy = AD8D96F01D3C12960061268E /* PBXContainerItemProxy */; + }; + EE158B5C1CBD462500A3E3F0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; + targetProxy = EE158B5B1CBD462500A3E3F0 /* PBXContainerItemProxy */; + }; + EE2202041ECC612200A29571 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; + targetProxy = EE2202051ECC612200A29571 /* PBXContainerItemProxy */; + }; + EE2202061ECC612200A29571 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE9B75D31CF7956C00275851 /* IntegrationApp */; + targetProxy = EE2202071ECC612200A29571 /* PBXContainerItemProxy */; + }; + EE5095DE1EBCC9090028E2FE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; + targetProxy = EE5095DF1EBCC9090028E2FE /* PBXContainerItemProxy */; + }; + EE5095E01EBCC9090028E2FE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE9B75D31CF7956C00275851 /* IntegrationApp */; + targetProxy = EE5095E11EBCC9090028E2FE /* PBXContainerItemProxy */; + }; + EE9B75EE1CF7956C00275851 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE9B75D31CF7956C00275851 /* IntegrationApp */; + targetProxy = EE9B75ED1CF7956C00275851 /* PBXContainerItemProxy */; + }; + EE9B769F1CF79C0A00275851 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; + targetProxy = EE9B769E1CF79C0A00275851 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + EE9B768C1CF7997600275851 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + EE9B768D1CF7997600275851 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 641EE2E02240BBE300173FCB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = CBD35U2N52; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-cast-qual", + ); + }; + name = Debug; + }; + 641EE2E12240BBE300173FCB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = CBD35U2N52; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, + ); + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentRunner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-cast-qual", + ); + }; + name = Release; + }; + 641EE6F62240C5CA00173FCB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = NO; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = CBD35U2N52; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = WebDriverAgentLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, + ); + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-objc-messaging-id", + "-Wno-direct-ivar-access", + "-Wno-cast-qual", + "-Wno-declaration-after-statement", + ); + }; + name = Debug; + }; + 641EE6F72240C5CA00173FCB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = CBD35U2N52; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = WebDriverAgentLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, + ); + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-objc-messaging-id", + "-Wno-direct-ivar-access", + "-Wno-cast-qual", + "-Wno-declaration-after-statement", + ); + }; + name = Release; + }; + 64B26502228C50E0002A5025 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = WebDriverAgentTests/UnitTests_tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentTvOSCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + 64B26503228C50E0002A5025 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = WebDriverAgentTests/UnitTests_tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentTvOSCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + 91F9DB0A1B99DBC2001349B2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(SDKROOT)/usr/include/libxml2", + "$(SRCROOT)/Modules", + ); + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + VALIDATE_WORKSPACE = NO; + }; + name = Debug; + }; + 91F9DB0B1B99DBC2001349B2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=0"; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(SDKROOT)/usr/include/libxml2", + "$(SRCROOT)/Modules", + ); + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + VALIDATE_PRODUCT = YES; + VALIDATE_WORKSPACE = NO; + }; + name = Release; + }; + EE158A9E1CBD452B00A3E3F0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = NO; + DEVELOPMENT_TEAM = CBD35U2N52; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = WebDriverAgentLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, + ); + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-objc-messaging-id", + "-Wno-direct-ivar-access", + "-Wno-cast-qual", + "-Wno-declaration-after-statement", + ); + }; + name = Debug; + }; + EE158A9F1CBD452B00A3E3F0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + DEVELOPMENT_TEAM = CBD35U2N52; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = WebDriverAgentLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, + ); + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-objc-messaging-id", + "-Wno-direct-ivar-access", + "-Wno-cast-qual", + "-Wno-declaration-after-statement", + ); + }; + name = Release; + }; + EE22021A1ECC612200A29571 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Debug; + }; + EE22021B1ECC612200A29571 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Release; + }; + EE5095FC1EBCC9090028E2FE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Debug; + }; + EE5095FD1EBCC9090028E2FE /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Release; + }; + EE836C0B1C0F118600D87246 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/UnitTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.WebDriverAgentCoreTestsa-"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + EE836C0C1C0F118600D87246 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/UnitTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.WebDriverAgentCoreTestsa-"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + EE9B75F31CF7956C00275851 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + EE9B75F41CF7956C00275851 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + DEVELOPMENT_TEAM = CBD35U2N52; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + EE9B75F51CF7956C00275851 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Debug; + }; + EE9B75F61CF7956C00275851 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + DEVELOPMENT_TEAM = CBD35U2N52; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentTests/IntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.IntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = IntegrationApp; + }; + name = Release; + }; + EEF988321C486604005CA669 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CBD35U2N52; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.yolojtAgent.wda; + PRODUCT_NAME = yolo; + USES_XCTRUNNER = YES; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-cast-qual", + ); + }; + name = Debug; + }; + EEF988331C486604005CA669 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + DEVELOPMENT_TEAM = CBD35U2N52; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, + ); + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.yolojtAgent.wda; + PRODUCT_NAME = yolo; + USES_XCTRUNNER = YES; + WARNING_CFLAGS = ( + "$(inherited)", + "-Weverything", + "-Wno-objc-missing-property-synthesis", + "-Wno-unused-macros", + "-Wno-disabled-macro-expansion", + "-Wno-gnu-statement-expression", + "-Wno-language-extension-token", + "-Wno-overriding-method-mismatch", + "-Wno-missing-variable-declarations", + "-Rno-module-build", + "-Wno-auto-import", + "-Wno-objc-interface-ivars", + "-Wno-documentation-unknown-command", + "-Wno-reserved-id-macro", + "-Wno-unused-parameter", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-explicit-ownership-type", + "-Wno-date-time", + "-Wno-cast-align", + "-Wno-cstring-format-directive", + "-Wno-double-promotion", + "-Wno-partial-availability", + "-Wno-cast-qual", + ); + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 641EE2DF2240BBE300173FCB /* Build configuration list for PBXNativeTarget "WebDriverAgentRunner_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 641EE2E02240BBE300173FCB /* Debug */, + 641EE2E12240BBE300173FCB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 641EE6F52240C5CA00173FCB /* Build configuration list for PBXNativeTarget "WebDriverAgentLib_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 641EE6F62240C5CA00173FCB /* Debug */, + 641EE6F72240C5CA00173FCB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 64B26501228C50E0002A5025 /* Build configuration list for PBXNativeTarget "UnitTests_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 64B26502228C50E0002A5025 /* Debug */, + 64B26503228C50E0002A5025 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91F9DAE41B99DBC2001349B2 /* Build configuration list for PBXProject "WebDriverAgent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91F9DB0A1B99DBC2001349B2 /* Debug */, + 91F9DB0B1B99DBC2001349B2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE158AA01CBD452B00A3E3F0 /* Build configuration list for PBXNativeTarget "WebDriverAgentLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE158A9E1CBD452B00A3E3F0 /* Debug */, + EE158A9F1CBD452B00A3E3F0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE2202191ECC612200A29571 /* Build configuration list for PBXNativeTarget "IntegrationTests_3" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE22021A1ECC612200A29571 /* Debug */, + EE22021B1ECC612200A29571 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE5095FB1EBCC9090028E2FE /* Build configuration list for PBXNativeTarget "IntegrationTests_2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE5095FC1EBCC9090028E2FE /* Debug */, + EE5095FD1EBCC9090028E2FE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE836C0A1C0F118600D87246 /* Build configuration list for PBXNativeTarget "UnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE836C0B1C0F118600D87246 /* Debug */, + EE836C0C1C0F118600D87246 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE9B75F71CF7956C00275851 /* Build configuration list for PBXNativeTarget "IntegrationApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE9B75F31CF7956C00275851 /* Debug */, + EE9B75F41CF7956C00275851 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE9B75F81CF7956C00275851 /* Build configuration list for PBXNativeTarget "IntegrationTests_1" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE9B75F51CF7956C00275851 /* Debug */, + EE9B75F61CF7956C00275851 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EEF988311C486604005CA669 /* Build configuration list for PBXNativeTarget "WebDriverAgentRunner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EEF988321C486604005CA669 /* Debug */, + EEF988331C486604005CA669 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 91F9DAE11B99DBC2001349B2 /* Project object */; +} diff --git a/WebDriverAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/WebDriverAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/WebDriverAgent.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/WebDriverAgent.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/WebDriverAgent.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/WebDriverAgent.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/WebDriverAgent.xcodeproj/project.xcworkspace/xcuserdata/zhangwei.xcuserdatad/UserInterfaceState.xcuserstate b/WebDriverAgent.xcodeproj/project.xcworkspace/xcuserdata/zhangwei.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..7d3725ef63666131a2ac0e9efcb42749e292b51f GIT binary patch literal 143820 zcmeF4cVHF8_V{;Z?)F>u_Fj?;C<+O^NRbYaPC_p+Bo~OJk(am@scKYpss>e) zszueN>QGNmt*JItTdEz^p6Wn#q*AFgDn#|8`cng_bm}Q;Fg1i4N)4liQ_oVPsWH?G z)Qi*<>LuzGY8Lex^*XhfT0*@+Eu~gdYp4y>MrsrF0kxCbMeU|Or1nsIsgJ0Ssbkb} z>I8L?Iz^qK&Qf1f-%#IEKT{W}%hVO>PwFo~0Rf0W3v@saY`_Bopd5G@R0Nel9EbMxX^~30i|Tpe^VOx`3{r2j~r+1gRhmJOf66XTfto1{jP4qrhk|20Ra5 z1QWqjFc-`N^TDfN0ayr@f#tvrR)CdY71##0gSWv>unX)3AAy755I71>fRo@$@HIFG zegnUQ>);0X1N;g8f)oT$gj%SBdT4^>;G-}eR);lUO;{H`4jaLyuo-L)JHk#d5hlUT zunX)8d%_gh7pB62Fau`7ESL@R;7~XW4u{V|3@5@#a59_%UxM@CeE2F{02ji=a0y%i zH^I$t3w#g04?loA;c<8Zo`k31Y4|z(0)7dcEMyD}qOd6ZUu5oKTnt&!oQ(jX+^N8kAO}wUxriP}brmp5OO+(G&nx>j& znpT+JYAixLD!+{(hcZ_bQ8KM-I8uax1rn89qCSV z7rHCmlkP=7NvF`m=;8F!^fUAb`dRupTBb2Qk{(5mr6l-(!1#0^cVD(^jGv5`Yio5{SEys{T+Rd{+|AkzD!@CuhM@p z6ayHZ5g3uNGB(D}I2aETU}Bh9<`L#mrYX~mY0k7@S~9Je1m+2*HPeP^%XDPAG2NLS zOiw12Nn=7xHj~2)Vse?m%y4ETGm06_jA5Q|}N^yO{&bC(J?SBy);6&3w+BVZLR)V}52XvOFuWBCBO}te!Qn5^H2l zteLg5KDGk;5c@D&g{{g~W9zdG*oN%mY*V%++lozKJF=bFr`UmP2Aj!dvDs`6JBZC? z!)zWqlzomJ%Z_8mv(wn=?91#c>}+;E`#QUrUBa$r*RUV5d)U3~N9@P!K6XEQfc=C$ z$R1*kvZvY4*)P~H*>mjo?0NPQ`zw2y{fnbGz(J1T1kS{nxktH5TxBkfi|49vRk>}p2krv*Bli>cGk1~u zg}cQ4%3bDu<0&5SkQaE7*YXzL%G>w=AH&D;LH;5BQNB7~gKxvP<=gS?`3`(Xz7wCw zC-I&6E_@F@h40I!@@aeqpUG$OgZUx+P<|voiXY97;m7k6`APgM{0x2-znWjeujSYA z>-i1*Mt&2&ncu?i;NRyz;y>p1@t^WX`Oo;T_%r-j{%igme}TWuUlH_zL68KaU=qxN zMX(As!7exij}V0CgmOY9p|TJs)Dmh7b%Z8DQ=ysATu2bw3Y~>6LY9y%TSwbL+fmy|o2X6F zcGh;$cGY&%cGvdM_R)s4nc6IEwsx3yxb|u7Gg?_YTKl4QqIQyYvUZAgrgoNgjdrbe zop!x;gLb2KlXkOqi*~DaoAxd3PVGMJe(eG6aqS81N$n}^m)ftj7qmZWf6`vnUeifB zqt2u=>nu8}&Ze{L96G1YrSs{^>niBtbn&_>x;nbLy2o_Qbj@`wbS-tQb?tRsb=`D1 zx5l1+>rUuS>Q3pt)P1M>S$9$Qi|%*bb=?g;qi6M; zp4aR2M!i$-(pS@0*VoY3)YsD2*4NS3)jy`Mr?0PXq;IKjukWDmsPC!orSGlpqwlNl zr_a)7>vQx&^~3Zp>8I+a>8I;o*1w{kp`WRrrJt>zqkmQZx_+g8m43B;vwn+ytNwlc z2l}1*UHZNH{raQ&&-6d*FY15MU()}ozpTHazpB5c|4sk9{!asKU<`VL!5|r22Diau z@ET$a00t`wH?qWwy|-577IVytScW~^?kVXSGaWvp$iW2|dzU~Fb=Zfs#}XKZim zVC-S+Y3ya}ZA>wSjG4wPW41BJILJ86INUhZIL$cS__Fa8;|$|W<1FKB;~e8$;{xMi z;}YX);~L{y<96c?G{dd?)9uxX5Gyy+#=RMRxmbkkhZJkti#M$;zKX44ka zR?{}qcGC{io2Iu+@0s?P4w?>`4x3J!J~w?~`qK2Z>741J=@-)_({%x<&C>^1w$vF3{AN6gjC)y*}`_00{;4b8303FarvtUgkV=zIm{D zhTW(nXu>5I-R^Do~nyhB4*XpzStpRH}>%-P6 z)~eQO*2k>%tclhnYiDa0YgcPGYj?%94O^eKK4TqW9cvwD z9dDgron)PAoo$_CU1wcy-C*5l-DKTt-D2Hp-Dcfx-C=#ly4!ld`ib?R^|$leLtmmv3tQW1nST9+xTW{DbHml8Mv)ddtr_E(^+dMX}&1Z|XRkS@~t7fZit6^(s zd)(H@*4WnEmSF2>>tsu`^|bY}<=OIWgKa}>Lv6!s!);I7p0SOvJ!>0j8*h8bHq|!G zHpe#CHqW-$w#4>^ZK-XgZLMvcZJTYo?Xc~L?Ni%P+h?|8w&S)Fwv)C~w$rw+Z0Bqj zZNJzq*{<7e*#5AycFxY*1-ssEvYYL0yT@L`UejL7UfW*BUf2GZy`H_ky@9=EIIouA9qk`ih$HR__j>?Xzj%tp_9Q7QDjwDBCM;Av|M>j`zM-N9& zM=wWjM~Y*BBgZkwk?R=lc-k?>@w{WKW1Qnf#}vm*$1KMh$6Cia$9l&G$418{$7aVC z$5zKS$6Jn_j(v{(jsuS4juVcPj#G{=9bY>xIDT~e2sEMR(8fY73=9?VRI$)mi9V=3MSv;oRWd=-lVr?>ylA#Cgzp$a&a# z#QCZ7sPi-DN#_~odFKz#3(m{VE6%Gf;DRpX(zrO6)@5;7U6oyNu6S1!S5;RvS9Mnn zS4~$fS8Z24R})uLS8G=rS6f$SR~J`TSBk5zE7g_e8sN%s<+<`*6J3*BlU-9>FS(|= zrn#oOUUt3Wn&Fz`DsUCLmbsR@R=75~HoLaCwz}SQz3bZJ+Uxqr^|9-S>r>Z{uAf{# zyDqwZab0r#>bmT@;=1a(=DOiVZqcoE>)dv?!|imt+&*`#`w{n}?n>?&?wanl?so3> z?hfvb?oRGRcapoayNkQ4yQjOa`ziN8cZNI9o$ns(mfhGr(ml#O*8PHentQr?xqF3s zrF)fowR??wt$Uq&y?cXuqkF6S9rwHLJ?_2kkKBjdN8F#fzi@x){>pvE{jK}F`v>>0 z?#mvXNAEFsB#+T!@|ZmqkJV%I*gbAftmhHWqn=8h8lIY-TAoIp#-1jgrk<9b)}BO9 zk|)EH>B;hBdvZL3Jh`5*C(o1b8SEME!JhG+37!``FMD3`%<#2&tcCI&!?W_o-aLLdA|3Y_x#|w;<@U%=K0O@hZlHx zuizEEX0OFt$y?bQ=Z*JP@mBR#^H%rP@YeL!@;>Hm>`m}K;ce|r^d@;bdy~CSdQ-f8 zz5TpTd2_vC?+e}+y%W8Yypz3Cyf1mDdZ&4(dtdg>@-FZ$^uFO;>Rskt=UwmJ;C;*c zw)Y+HyWXANJ>G-fL*Db=AG{a5KYD-i{_MTz{l$C9`>Xe|_ctHq<9xhN@R@vOpT+0% z`F#OjjIX?}qOYp2n(ql;YhN2*TVFe0dtV1%M_(sjqA$tU&6n)!?;GGt_YLyp`og{u zzGr>U`DEW{-#FhC-%GwXd`o@He9L_+d@Fsce5-wHd~1E{e4Bi4`gZzu`F8sb_&)I+ z^qusb@}2g5?mOfA*7uX|XFuoX{eoZgYyCRE-f!?rexu*yxB0#P^8O0`hy3yWD*mee zdj9(U2L6WrCjJ)wcK-JM{{8{}bpKQSf&L7Cra#M{?a%QK^5^@X@sIY8@jvgMGbkoMW^l~#n5Sc&i;-hS$Bc;? zA2T6lQq1I-X))7dX2#5lnHMuZrXZ#;W^v4tnB_4mV%Eg0joBEpDP~*D_L#S0-ii4j zW@pTvn7uLkV-Ca|jyV!@EarI3>6p)B&cvLJ`7Y*M%!QaAV}6Ob6mvD^TFi}@KVm^F zjAdfkSTR-`E5#aPt+BRPSFAhM9~+1*7h68IV(cTaak25S)njYK)`_hf+aR`aZ1dQJ z*tW49V>`!okL?|s5*vz5kIjr76q_GAEOtaJjvW&_K6Ya4OR+D<&WfEEyD;{(*f(NV z#IA|m5W6LIN9;SXAH;qb`*G|iu}5N$#h!}&GWP4(bFmj>oiCWP`?_ zDQFH_g07%D=nuvQ9}ZRwRti=NRu9$+whOiob_jM1b_ym2lY*UtU4mVM-GbeNJ%T-h zy@I`ieS*osfx(PmW-u$59n1+13g!lf21f=*1xE+R1fLI%4Zakd8k`oK5u6iT7F-@& z5nLHu6v{G(hUSt{40O3mgiYBi}*xn}idH7nPs+qhcgy49N0tz4@~&BitBR;^vL zTHR{Nk|m*8^RC&s1H(Bfsi9`ssri|qti0%jT~v^&w1jd~9?DDkC_fdTVyIY|mKm9q zIhmIQS(LR)sB%}aCrg#dby}r_+ot!=C3)YpE-4xL zp>T`b?96tFokHQf?A(;T8KFo{xLHbGO0r~CXvH?Lf`dez#Mt5>Pk zxJ9!jHEUOI(Ii=_P~0OqyGcrJ*OYLV^l&;6rfYiMfW-8y{uv?iO|s-C4I8KCkt}!d zaFx8C<^_Qwu zYSs)y=b|pvn5wjxdW@<^)u$Rz4XMYeMzT>h$!6IiTVPPL#~Qsv0V zJLG|Kh8&jj$j@XBBqBuOfb1ddQU<5@Pa$Kj9xDE|ac+o=ypgo(oR^-Fo|hg9cTG>r z8=y8z$O`9>tbTUx&1S`C-qNgjT6(li)!H|&S=?jvemYS-l>14flBmv97pg1Ojp{Bt zWtZ%hJ+fE!$^OMuPpTKyo9aU)Q%_PUazKug>&SKGCUOTkMY*3xx}|2Ph2oUE%FfSC z4HZp(=Y+Uo!Qz?@NXhCS3MWgk#f@91=iSmEiD+M3ThdQ%7Pc*}Y2;#MUu*n((vvJ|+(x5dg89jAd*4pnIp zl|f}vSyZ+hBge`?x!fXZ5S2@XsX=mi`62mX@=3d8OpX`fn4Dr^&BNrm>StbHA7hh$ZqP7 zp>VHuiOtdpE!kNqxx)%bt5MzBCAN=j3#l-akS8&lXd7u5y~gv@ILfw=8Y@>^NR5{t zQOubFP?_f(BnEOKHHn&>EV+m}C}yOCvSHOJl#?3@6JwU5NFi5}A8jwK?Hk?80uX6v zh9&Sx#DwNGP9?J#4y6&>6Y8Fx7s`y>d+%_OHYAWE;XdJ1#jNCch!{JE}F4IYC82YHR``^ zpDguFPReW9v`f`m&AKGryav(QYPD|uddnq~hUJ99wTmv7+$Ax&dexilA|sbbEKFAa zgfuez>HX4)F;;J51~oHTGPg@?o1do$d{aXTsTqonMCX4t^{S%tbEvu0JZioiFISPP z%GDH|Uq}^1biTS=;{oXWUDPfr>N0ADqN>Z~nhU9wa;^WUs%wd=uK!P}TKk@>YUr6d z^$Pa?x2pQW+Yv4N|5Vlg&M-y?ZZq{JRp~Wq3$>NnMs25d$dAeOI(&#&*fGNsW0V(yCT!x z2(xZC+W+^$1CCG1&Cbq~!5gC1?v5$<(4&>qNc)IRZ;*^8v&CvF3aWc$=Y&Y4nw_f{ zI}(oOWe*HxwMY+Tq^Vvk62KDQnj1DhdA^=jNsiEAR!1Jt;XqHn>2h$w}oa^eHy0 zk#>dUeF5_Q!@-3WNvuw*@$2ofB&+dD^W5C*T+&J9I3j#9aRO>PV$+ESRSG;JHa&93 zi{h(P4gAMe$&&MCH%ZFK-Rzz4$yMris!{=UjrvXQD0eELu2VPUMEOa@$T$b( zhjNE?NKsaB1dZ^`HCyj~5m-8;yJ?B3#7gvt9E@B^?k-bu zZ@JGQa*7=|h%&l>J1LL!*NxmzreaFuUUCnKB1!S0v)$T6KU@96o_u( zSxYu=lipWCq^-{%Y5M}qJtzUvIwrP2)JhuiO}S_f7M(H_uVTmE% zKN9`E+8*+KrMN=TUAbG+?99xRthBHMNsB5)EjHKONm^9%7l_sSbRfe*tVyd-3dsJQ<#%!0%&1xcODSh9BQcFU5rXdMu7%jHOAW_?nHnM7q% zd8E>E6j`IsAd8>v)cd58axbZ!JVV^#d19tDfF}miPHbNsAecv-aC6WKJOSE+ZjnmJ z0pKZ+4Th3hNDL-K9P?~a_gDbdfQ{fS@II+^+zSqaW2DCMEcgbT2fvak$3Gw>m5ma# zz=vQZQqTApYyjJkO2!_r7pY$ik!r;eP=@2-RQNKSf18u<9(C_U#}@}1pE&71u1#+(1A_erlJ+=cW zO4H(rirz#BkW|r9672K`13)?%3NSGIYU4Mf?y;qRv%vn^3>Jr0h=V{RiYp)TdxIA$KiN+jU?) z*gzQG1U7>$U@I|Y(H(_0*=pF;=jKf)<4NYQc*Wa^tjx&5BcvWDXJ)3PXWeX0;>O4{ zwC@+~^X7N*%kmU?raVWUEKg6C;)_@EYQm7L#gqw9JG{BVE?(2C6SD(!UkKimUy^eU zfp@^W~=2bdJt}jm|giO25W2&U?1|O18l#JfZoSYh<73oiUZUNY%L`rvU zV}9;0^}j>lVqupb-%=ALOqdo!IowdtOho=NO^N@kAyiabN6lBI4Wa1K2dA0MyK z7mgpUB)x}((&M8;6^>Ujl=`~5PU1o zy9xdG;38G&b>iNB02jcI;3x32JYRlQULY@&3*^Gr36z(J=ld00p~@397s;=YKd+OY zh&lF>ZJhj!G-WfV1KAww6e4SoaGny(5VIEUsi=GF^sF?J{=dbn_Et@TVvnmc*dd*SglQ#?l4TI_htNRagp#~W&N&9n zBm}TR8?=+-4(Nm~=!PEXg+6lW0Qor<28peR;!<#hCONmGWkcP@FQCKa@Afo8;wW zDEE?|*UB49*jZQ}R#5FMd_-O&uP8EqtJU$Uq>NW(d1bLNgH;q`Ca)^Pc9m^!RLgP* zK1N2l9x#(&tD&+3-8Us1>Ph}THDl?=qARNho2pre zN_T0Wl#<&&l=mN-N6CraL(~qF*ri+360-Vbt6h_kE79!MZ=~BBtExnT)rGLT z{Fa<^81^FGi1;Mf8}@<8@X7A#{uo&g%J0hW$ZyNL?xP|El@;Z#9*0&&XDSW$Cm}nD z{QJr8$?q4y0We+uK;9`=?xNmn7j;ZY5OVvCk?qftUXUyj3NWDl1l1hR7pFt!p+BE2gP zAu4A9Tts4eSSWwC5WXfKyGu+D-yktPTnd-T$K_MS?j5d#Yj2OV;X0~3+#sJIH+fR7 zP&|&oMx(3T8l=Om@Xb=EzD1RT@5rag$b2qWxH+uBMq@Td&Xr(P@?G!%soH_N;fHV! z+zUT~AH#iczxvIQR<t- z#xwA&Y90>3Z{c^u>Yf8`NVX@8?GlUD6L|yLgtGca>T_;67U4qz@(-$k_yJxZ4!ih_ z0{A0|iElY@GoHH$|Dr0bfWN>?@K<;lUV&HPHTWC+9bShw;2-c$`6u~j`J()bd`bRQ zzARsnugcft-{jxr>njk20D>TwDvxOLmqi@nk$^-Jx!%CQfQ6QXYCRU7P(E5?p)D4A zVj&X?weLZP$} zsn9V*pKbQKwvg<}Fjg$a9Vh9!2N+yfQr0j2yEdccv zNjDZ%?2^(ZOI6yCJq#t8l0kMw(r#uc)Z1%90WQR8VUCPOqP_=p?kR6B(@}8S-L2B*!UJrnrB1?F?u8}^Hv$EC^BF`mc0G% ztq^KDI=*zK^wz7_RId(`rR4i^bwy|5A|*<NP;SeaK8m9 zYeY&2(oPjbn=B2v4^yKivPqRpe7B~Z6Pl3~PHO1U7EQ>WNJid(rUOE$14}}hs7j+x zmNM_XG@VGUnCxT_L(wWJse|GV^2rWzT1Zh=H8Fv@pq?beL|sug)E)J}K*GR?fe8ci zBGe1@Mtx8+1{Tau!TemzZ&w_Ux|UN)NtNoKl71+zL#TgBI6oY%CsOJsABrpw?$lnX zKUDkpzb{CWrK)$khPut2QBuOo3I?edgfQrjK{^HlG04Oq z8-qa@gfYm+U`Eyo|vN3}#_4 z2ZMPSyo$j>3<{N@UX9kEwP+n$k2auT4 zGw3Y(8hwMlMc<)w=zDY?{eUi@AJI?fXLJ$$f-a$7(PeZ6UB%!v40dAhHHI{XK@1yV z*b~EH7*5A<4Tk$LynqphQF)9SW7G$uVHi!vXdOleFglMlEY?)On#Neu3u}g8%`~i8 zi8UW$&DWTQnD$}10j7IkIv>;1F}(`YdoleDW@yaBV5S~sx?m<3Gm|m13^Thha|Sbi zV%CM(TA1yC*@2jS9!6Soq|P>Sy2JCM{OiP1B|@#!4~WM_riCz6;*J#TQWvL&jWFd>T!RcggzeKE~k<=UT{S7hKt?{nLs>(r1 z?w*bgMetm8r@r^T;c zyj-$0@_)T}$qe8fhoePGdIniQ5-rK@r!-P@y^K*QrwT^0!uWv*rUYU|n%p>#6ovK8 z&kKdikgK99*TczD-o2G8vUVsL7#7JxBE!TX={Y1uE2%hfswj^nOZoR+l;X#Wh;`~( zy{T%d!->PP@&<&$WWPSQq%74{St=z<&)r*DB2!S7$t)V2n~8`rCaaDr6{*4vKOm_Z zlN3f?8Im+mC8?4ujd~!GbZc5PK8aZ=Im!yORVahFf-)p)qDn@dTpaT^Nk-~cl=Mm& zvb9uYtC1|p4@5Qsni>z>=8s8z*DXPkrj06Atp}%8#JLjul@Tf_Nk>(ZIu9;VA+|vA z^hNfdj5+I~iu72rH12`S*)4`!)$zN}T~AfK`pMFO|5dy@dE4fLN&M9^M2az$#~n)g z+b7k_H++DqLzJd%3VG75Wb#Q9*=fe0QL;4re;tUDo?mffT|(qR<6IJpre>!!4iC#p zP0S`$B6;yGn>M@U*OEi?lsYs`?)TE;KjnTj*{UGT?zbRiWy4C?nLJgH7WX;rLmn5; zAsbPoPHb34c1l`F3o}fWp;fXpKdL7Gu!Tot(NR*Bo>he=HRFS$Lf_{gpA-i5%}&WB zk2;XT?vf)oN)@Kf{Z^;4mejZAG&SQ?LE8Nza+;cnsx%#vrPTj*j_zcLizE7y+A&qV zGI=)O+5h#*B`sfM^2>@QLE-NlKOm-dmv*b9^(!Ta;{3v%Dqsbgn8v zmwWD-?z*y3Bb$NYtRxlWwk0wpZlDqD_YEyVvt8n|L3rwr1LK6Cl!x*TMGV^dgYu4<&|!#${lkzU#TLI z*A9(%AaiodTDwIPWgS(9o_wpyM2czdZ&gcCrlxrJs07wOs6q^WP)UeLj`WrQJbd?H z@1iQ+&}3;rbl(2&i>k!*%zUz&LoyBbep9EU!@Z)4J^XLj!znSl*Ht-4o$dqK8@%<} zGJg9J} zI7ltp3oT z73Y=v9iJ5~*^A?|TUVxZV^xTm4^DX!GE@31MF-0B1mqo5rP8!?3st1q$c>){S-Zr&cNVx3>IUs1cNs)Sc<_i43=ZC0s~USx@sw%r9L-B z=h9(1k19_OrpjTkT77s5gS8mEgTcGxVHfZoIY#P>T{nw|)so$~;%6tx>i|lBNuG6U zo1R5pk3rV&F=c+O)I2xMCa>%vhJw7^t;{wFS%cMg8Iz|@O81fxdGAK?qsOF8uyikx zly2myo4D+L{lX!#P%hn5_?A}6dr9wfOT_;Sh&+2&`i_-?z!upVr0A=ES)(;1HzlVH z=~;aqoE}XcN~KAS?i%GaD&zridK~D!h&(_T`9mHxQvc*c=Xo+cm9iDkQ)p7eyAFf( z1@tshwF)+1utRx}H$W;5hxI3K`OPA)7zvYjC#*ieSUfb5r;swpBW96@yl?w5dXZW5 zJo3;UJ)53G&&6OP2AeS0Tu9HSU!@mdumyu{7;INASE1A>la+9Z>nD=sF&V`8yEIJ7 z9@Z>9temJ+P!_%NTJn`TBTTjm$g8O1hA1y`K1jb##%eLWguI?NEhu-kb&76d3uaqN-yg|9=s-1{{{3?@^%_}1?gaA=?>nTSZ!cx za+Bkmx&&-PzW$lq`qp5hv;~y%Uvz-h(i@anSVxlu>6;k5RX}e9oiKQtJnmVcPg>FI zEz~F4dMa8Jd1fsXHfqFRBQDYtCF!D_y+yyPboMq)Kzkp956CNf=#4U|>wa6V(77K` zr0{KBMo;{ZKCCdmhu%wnM1M^0qxaJX=uhZ_^dSs(VXzy64>8z-!CnkL!r)^J_F=Fe zg9A(HBSp*~qmR=k2=k{D=0Axte*}XsG5AVh{>;D3M?fJaKYPeMWB$Ct{0|r$j57Zd zVV?e(zNq}gfK1DfT)>pdi`74jt>Dar~ z)K^=sI^%!57I_?p7>BAwMDLD{Mv$oxA(|;4A^I#Knqd^8kC!qJ(NiihwF%2iWhRb^ zXR0t&nQBaRrUp}!sfEEe7<`MtcNm<*;Cl?tV?cE00tP=~@Y7PJj>q!mNUse zKj(f>PU}(jV{P-S2G{ugg>)tu$51EgYi|iqe z89E}4`D5akp|dQz; zW-WwdmarIKL^A8R+p?PVv#~@q*Z>=YVO0#P6|g~;%vW^`BmWM@JqfMZifkq2B9ABp z*H8#%D=P%oEQ?@GLXirx)!EvFV73NZlRR+>Yhzdk!@7lR9k#AQ@M9R(`}+xI8&L+f zacL^M9eI68nj3==SXVkw(k#Bi$WjM@GQ!>ywXw=H(Y-Ty`SJ|LX0AUqgD zV*ewVF?QHp5ti9e3SF2TiD5eo+ZV8-*)bS)z_5$rgzrf_%}!t^DKNgkzQ|6*up@?q z-^4<8GCM_qF$u%Y{{R?g5Wr@Z1`MCx!!NGhvicZ}abMw-s>>oU&LM5*mTr4`!KO~T zH#DvG(t%gb^w2jTNu+2z&A!SODC4|9v0_~#R;*C5V%^HJVkjC!vv07=Rc^^;-^Z|r z!fm0#ZMQpeyO!OdaJ!CO&u(BhvYXh=>=t$_yNx_)4|`!q=o z!!WdzeY1$$cZ;~)sc_pb%I&~nZioEK?S0_(u)^&T4Esm9{fuzS9%GM_zb7#qfMI&% z^C=QUSCBYK8@26Uv0p2ib%s5QVFreo1?)HMw-{z&I7lJ*-ZH@ zYAlB13b^JRiKND3_~Jd|mTS$mi*U=ejc_}Gs2|rs;r4~n+@f+(4eZQyC){#fxUO6` z42jF0gyG~ut_Rms;dTm!WE<`8&VO-z35%(vS!~^N{wH;>H{ZLX{)zM_9{rXi(FwQx zNZbCU+it1#&G33dnp|GLV0V{eJ#G27aQhUOsdAeU;da_x1}-{kVeVOl+dM9x8_W&i zhH}HW;oQ^QGaO;{Wei`za0Z4mF`R`VNodW%a4v@PFr2@Xd#;GvQQT;53{{>Rt8n{j zl-q?EuE21mvg%#+FSz#s+?N$_$@E12O%Ae|%+0zhxC^*K1>A*P0fyv%g$3Lq?llY- zVYpNQ_nw5z+){3(g5WZ4IY%a+bpJYriwn6`+-e2EB^bW(4?u7e8LQ2uv8tiJ_-z`~ za@^YFa}!Sttz9{S;5O2Bd+D}y8>db_`g*HVoBuqwwB{-}?iK{!;vze|>p8N+OJa^? z3WDz|wrhE5+ZBunHu4m$ddo+F+{^7#8U8rJ@ansC5IyBE_qoFG5$;p&DEAq6j62Sq z;7)RUuP6v##c)T|_x?^0=B~pD+;#3x4BwQ$ARic#mzYb{C8lV$dBihBL3j;MWB3k+ z?-uYZdD$3z55t}J%mwp0o^0}x(3vM&yhM%PCtApx6gB>!v>LOuqoUb)H{qXm@J`-^ z;Vuk!WB6eq@8P`)|9ddp`}gzDm!k}P`O-|kvZ%?F_&?kKwN+D5=3kzFTH&96n6#}} zy6yWL7fo;3t;xGHK3v5W4jcRJP5${xe0&67J}!dq$9EaH=&04?8xef@T6}H34quml zjIYPn=Ns@1`NuKbkKq9fKf&-IhKDdbj3HTVeTv~x3_n}SH&*fGn==jgmQ;B@LBaP} z6kkF=!0>y;1)u+y-+Rw5-<9yolUVb35x;y-!Y|*8?@j(DV|W5XvIZufr{o+XJTJMV zV06ku`~ZdFetdrnKgaNk0zRF83d1ikJbUjL&gOGfhDn(NVfZUW74sB^&y;2u{}dH% z7(YT`csTzwPgZPSWB3h*-xl)E^3N#@e~00@e*nYJ6E4S==5oLh`7`IBrUN$3+A@0i z+nt9;7@k1dzEHaDI^&z6kuBSSrU#MU_i=WNU;pg)6`1$;+`~rRdbq zvhl0D%${GOdSQOKg7LK|#=l3sFk=3t_&!j)RiStrhQCEAe)DdDH2(p=TS0dxPu$ye z3~vYD33b%(8oA*~Kn-?9ms{&NG zy~h8>|IT0MZ}5NcfAW6`lt2cM$4J0P#7K*g4kJBA28<+(j2M}g3P|NvU|GcXrpgN< z;TD;TxJCBJzVAumeo^pWaPK|1f|GzNxG=I5fh%|kxPnjcD}OPvVq{Z4N&H-?Xtss& z!ox&CgbKn#7&$O<76=uEM=&DYc<&j$LcCB-xkwd)FLEpR3e^d|$Ws=+w(U{T>Iw}A zzQSWdJ)u5EK8(ms1PX3m3b^+qfh9~5NZ3~>OctgHFJV**quLnNDHNs&(-j2kV)WQQ z070^kE0nR1`~F0+!Qh6?=51`ceMe#Hv|SMdUnOl9lt%FVuC1q3{H4u|7xvq}ucZI@ z-CGbW6v#idS|BV^Y*+n=?OLqZt_Ee@M~u2#Y<$o?T&I8xzus5ndH3ZeLKijn-?R;(87AZ@otJndy7@JfTQ- zc^8VaF^bk}i1Y5s?ILlp!tHC~>lls2Xk3A~M0^7y5;#v*T<<-Jn#GmkT7|$>;%adX zMiVf40izcS#dYF(g}{jzP5K8oQL>dQma&!l+V__#vV&UvxuFi;tQq-Rj|hQpk+yG_ zCU9`)6@it6wKNF9M$Hf!kN%53;TKru60;88PdIh5y7|q0J7Dhx%=3q1z zqj?z3Un+i8#O*iYx8iqHdGULN+i1N8D#U0NMyr*WdCk8L_dYn>YYMo(VHB;`5N{B0 z#Xs2N;vZTHBT_Guu6&{blCAPf-lF-|(prwFh?dc^7%jr+wE``#6)++=lY)YK1hCei zH4}iflGdm-VYC>dB^bR?sI_RV3cyP-B9**mj{vZa1zuL{qk zZLCb3{nmrp0BIXjy6v&m)y_J;YE^z^%eAk4d$Ak139z=D_Mr&C+6obXR}d7n6%~M2 zmIm0Q3pRSBD0cQmC#s6JdW26^DfkqV3Sm4M1p5+^4*-Uwx2d#VYt6`07ma%^lpLnDeXXv-oxm_ zd&h8&Hcw%AkTzFK0^s*C`T(Pyh1z`WV1?mb7?GUp-_Gy|!sWB2xg5B8WzSl%t=ml) zJ*WQUI)liA%7kG|+Kw#U_WeMY$rb6=HQ(rzwS4#I&y$L>Xc(*=qaCM=^YcpTiaf^f zR)KcBlDgVkR_cmL2sV1Sh+$h4$d|O!RfeZU7~ZciJYF#ZdrKLC=qavSLea#Y7x6SKVerF0J+LvjIPOK{YgGZARSXQ)w+jtj}Yz9 zJ*=yU(eD^tFVH=ztAx=Fj7VX?J*wK!Rn^sqD6X!0L~;KhDyXZaC@y)+$8GO*GHQlL zMXRTKoDi?8uWO)dh&2?}0IY$9x<P->)MdEZA-V!+^|S^_qnEX=G1?tm%DO(Ws5jE1RZpV5q5Rt?L+Tl4Re=) zi=NV5mqyst_0aXy_0sj$_0c8kp46r2`sz}#hQk^jYXqzju||tEI;_!SjR9*UtT8Ut zg^Jis*FB{hNR`)R5_UDFB6c-4tck^%Ac>td<^F~DKEV650`D_eV~)c6oC2?`!#bH1 zO=>JyV^uyQMU$YC)DSNuHFH%H6p=h1VO7J3JRiZf(nWY;EJfYqvEcpAn^N~B!l!$?epdJt1tYc&;5L& zPS4DlbI$cX*O{}nwdrC6W4pZ6YPQS!V?xP2^9~i{ExF3kaKxL_?m$>?XMMa8tx0Jr zX2+J)XW}$tLq5>lxQ0v=Z+SlTWk&PP)Lp5&Q}?9qO?@GCU+VtU7gJv%X;YFmBWZJz zwjgOslC~nLkEE?h+J>a*m8l03G#^TM;x`%1ev#(DMNeEehvQ%9jZXcD!Td2vGenp_ zt?5HJ_3Ko&D@*D(sW_=UNYczm>UXK%lQfH@Ikm^|FR7>F4F48mIGdT@AB^EpGQ%nN zixzlJnZg)8pL#(F7u=Sl?MT`_s;sB1&lv7N(vDZ2VWmQ%QZ`6tc=xbV*RDR_Gi&Qr zo5p;caTDfF?Kq;uk}#zzIc(`85n;&#`KNE&zGn9#d6!WLo5%vCR%wWFtJKH1WivSb zDdWUjY|19!R%ur{luo5f=~jA_X-coMk+LyKb4i*<(tMH@khGAbT}axMq}@o`ouoY~ zl}+Q^DqF_6RkmT=iZeK*y)SY*xXuaJz7tk<0JloCHpK}X%5Y8GD!VFsz%rEGlqkDC zB<&kf_Eh#F=~X1XhH+afqe#kr$^jryS)}Z*yqcu_NLobF{!!&M%7KhPTpI8nAg~xz z4NIo#(M|97fB3~d->rOX+w6*hw>rlN9Eq@_lEd!fx>j_b*7dVx6L&A}u({tQbFY+R zl-R-#U%txHSk*NU1S-d~s%ubk)nzXgA-q9}oF7%3;sm$L65QUxxE&+< zTkQ6b`0|}v$L+ts?R|{f`$;-B;ct}>g4+fs8}?wr@Ev;n04xixOMTVjSgfteuN*H}6+*>DZ#MJs$xxtkIHymF^< z7fC0R^ahfaMU{J$dl~UlNLv0MApQU-KA24LOxJBWn>uuEvwC9FEfqJMS|20+5W>Ee z9M-=}eN&A}PMH=8$wQN#~KYf}~Wb{35~b*9msNW9-fs*&kqzgoLFM!>KJya=73|#5L_%Hgc9!(UsN>C|aA1awjPSV9BT@q0> zP&Fj!Qj*?MYo1r7QK8`%RcTc^6+XXjBIz=cE|02=Dih;y1xatd@*Jw15|zr8%$Dog zE>r5gP%!z)3DfV(d8_pUj6+o#!g`a#HaX0{e!g4hnKzwY^T>0vMqwvkkwaAzRdbfl zO)+#Ny%lv>MAZUAN7CD>8#+?ICYDaRDn90`%81c>2k2F`VAbAjN!6ZsOOA?-`KsEg z+Ns*BI;c9TI;lFV!m3;q+RAs4^lp-_Bc!|?E7FU#nF-JPSRK8!Pp^s%`KqoX=>rLRRW%Ozsz$5WkgsZtYAi|Dku(xfjZRK=9IJf-5(VdEuAiz2>YqFmu|}ZmWNf(Y_pES0sn+H~B8z zk*R$iyZ5y2gV!}Pv47n~+HX~1nFm_Ts@UpYDVyx^PZ=lX+A0-W-K)A+wOX}Cb)V{f z)mqg9ss~l;R1uOsLDJ16eUhY4k#q}5QH4BB(q~AzjilQvRqGSnCRX>V9%I}-D{_m4 zrwMLfuH*J!;FhiKRXs=29U`|iuI^R6s5-#7eM$8)NuMX_&WP%u>J^frdAh&${Oubm zH2R{dH&usKM@YJxq!F|^)@5$1(NRj5BS^Tpz6b9s!}#Tap>7+^Fuece`~Z# zVe1+r5Su!yK1mL%ePzwAv)jYvTQ_!mzt1AYrb`I?T*a1zsJ>wSmQ9(As7^9}`%-oO zHf5~{;rFW88jn3Htnt`S(gTd!ldQtS&L{sfz$V`EyPB=;RsEs*Q*}mlR`r+aoa(&l zf?A?ZAt?&>RgxYeDcWAIlk^RezDd%8a>p&_wyPuPRzAy z)NFOHdZ2ocda(Lh^>ykY>Y?gl^)U5tl730juSj~5q+gTt8p>1vx;+>6yWF@Be*my+~1lAeyJm#LSN^mme;t2OtlzDO0hTlJpOf{z=j^QT5&Gm5ju*B*j@NS9+$T`hL*0Hrb^vxnqGOdvLEF57nD-PnY+H zGM9Qpy$)d`$zex*_5A+Hhw|GzJpBGgZe3%-S#DwjSpAUtVV2JunEQ?GFQR^gx!-E{ z7il2wu&I7RjddQ3-zOQrTngj&5mxd2gP#@&`!~GrtzZ~WXRIt8kPDLCxR``Ye ztGy23+HW_jk1>EhAWkW|UkvsB^I3)JFVrWQL4B$Iia0fKnuz*q^*6+6i8IumYte7<#f#ceue#U`eeuJEfedMKh@_L?q}3z)qfFZB+f*fIjTOdz5v`g3vt#f zkGqDKs5C+{!mGcy+xyd&K9x_aPmgLlXh$=Ky9T4cnrcRYTc0XkI%Z4f-I2Q<+IGj= z`;H>4w{xTT?2JTSXjB?)j9`r>MzEuX929RcYuEx`jYVVC*fe&HL*vxAG;WPYlSZ72 zI5%+~;_&ZY;s9u4;+hcGl(=StgTm5b6P9c zHiT(t6=<>qR+cph*a%bC@xSlTXW-WDxA8A@Lu-csTrFx;62Tn~aJ3_}BcT%F<`cId_PY>! zJg4+h>(nN($gkHpoib?n-@(;l&8MBsuK)cfQ-& zu|=OBp8DzFcPU#(nJ!^?nfB&5!z*G8-^ysbl`)(=2}9E%OOw)GETFr!tKt~n6T_J8 zzVS~tinpxQKEg15K>MI}oi?J4YS(KY(r(ah)ILny-NdaV?jGV+5qB?ftBG4f+;laq|uBeo)-tq;@2P zJ;^pRirFtH7K{2-Ejx!pdr12laS`I85$zk=H;G$MT&G33f^Y*9@+Y5sO$Il-qiQd-k#SVisAbu!hV&E zFTe8Lq6hcq86G^`Y(e&fQ8?vZY&~ng(S8@l_f!mDc3_J32ZnDo2d32jNGzRSwZF&N zJso3rQw?{Cx17_-7`x}S7jzO`imslnzD}y+bi59I&nWsQiF=B;EyTXZpC%3;Guw#U zPTaGVI(eL3T`EpGIxlIg(=c{-h%R_%?4+aJ%mp8;gSYnK)nRq-I-Q%i=MpYh=LNj{ zHeF*Tc1Ab%eEb(5zkL&>t!t@k4Fl1&(xIPm7je5Ix;DCW;`R`?kF}s{<$yt5rY^)H z$znFM7v>w$<*+`S7pm*SY51V{YMpd>fL_;G7uKPpWj}E*68BP6m#-^e(7#L^md0Lb z=yh1ctE*-a?=KZ|+phNXS$g}JtH$hXzVKTHy{<39UX={JkT>Drsdag;-g)DpyQaOh z`2N44*Y($36T?@BH66%+SAefBhe7{BQoCEswIRB(z*jd^SF9VR8?GCn8>t(mE76VC zjUnz3aj2zVC+-d6P)8po?g(*5iF=E%v%ghh6MEe#I0|{;qrH<3R4!8Rzr*8vy)v21=zE<$+1a^p{)U6ApI+AU&Y_HAy#`^m$zhAX{rQXK z&h8_YtorVgFN*HRkogWxgJ`0N^hUjf)qs2SW(G1ZWgwqnHQ?E#8c@8&rEdX{^=`dK zpQiWf8|fSCo9LVBo9UYq&l4{YFC$(~yn^@!#5W{9m3Y9Zs?@iPgRE~8cfk4pK<3p6 zka?ZxfO%IP$Fv3Xc2FR));i{`UNEbLzTzOWB{ zZ+#!)^~4(@`m6N)h&K{%X9(BI*pdDk{k06mf%-xE!Ni-0HxqA(>aWueVJKRO#}uC{ z?NIfjfLBQ}Uh4NJq}+Nichy>T+p!zdPoVV=6vrZLX>!;*!yg=eZ+!2fb<3mKQ@{M= zk`=%D@%l+DpC_Oe;~lIP(@#b%#yhL2#WX46>FN4udi44+dZ)+eb%S30WYCNI{LngbHYUCa@lA1_Mx5HwI&w%;;AJg#lyY>4R>U;Ei^)C<~BtDb)tf+p!{zZm* zHu0hV0P0x2tFLDHu6crT>^O6u_ZA+z_J{JBhv6}S`kM%QI2mKi)W z+j8M@gcS*VOaE>h+IM2mw*7y|wGZ_t8MGhiKh~enf1>|X|C#=C{TKQ#^ zU1vaBvP*(mL;aelH7E?JFbzWk16obpi0>XTC=Dv&dk}wBt$AC6-e3lT27|$9fUE6E zd@tgAM-3K(l_3Z}k6HOw+S?jD5|tq>8LI|+N+&i=>AijFom;unr(c=F5HvJK*e1zg zEw4|XHNUd3o!W2NFMsc(i%0CH^|% zhY&xM_+sLRRT{d+!8P>cJE-SM8XNjDaEFWFjv^fCEB%>)JGlBMju<8yU{YguW7>M}miN`BWh#D3c7Bb`~68|5V;bFk? zT|+g?cVE8w;I*6k_ljBqxGeYQUM)y7UUhur>q4Bqb%_J`!Ki`(7s#K2ZP?q3srJ^u;ss!D|Q zc1}yocJ3zT#;=Co;~bxkaePY+c_`j;&d3IQ4d)FPj1pstv7WKMQEKFjyip+jHsWt5 z{tn{rB>pbq?`y=dg33t@=O|YB`RZHGG|>Db_@<( zmpAd=eTSpJ(K9;6q!9~#jnypp-IZM&XtJnV_g(&hr3IFt;ctg(>~8GEK-_BV8LP}T z0z_jUR+&AVT$ySA5=*DQ@tPRE#sM*WD{Ht*yk&@y4fq;|8jFp?jKhs1j3bSsj3vg= zMzpLRBmQyXHxd5?@tcW9564r)Zy_GNXirxf69c}+3EVN`B;d;ieEDZYe9>v~3Gw?G zzAx6{`!C?jMtqGG#BUSvt#QQHxXj2#e2vSED~NxV_#F}BEyi1ke~$QFwdR72cNy8Sy=PX!)1^orM=3UT7ZEc;{z3W4L3)*I3PnZ*bu)=Zfy@^VXt0f1Mor?!)~M zR_zuuVXJXl9NlMP=)T0zUB!UknFPFe%TD6~hVCxoZsQ)~UgHbKea8L97mY6&Unc$l z@dt^2g?O~M4iWzv@n~ z_g_HwV}|Yt;*W~xepXYzmGK+ncZ{lUji-ozoA`Gk#_x?k5dSXm$7&DVUyZSr-&P}9 zen9X&W`BP&`+Gmh{xpNc0W9NrQ$2?61(U>-Li`8BA1D69sHwh5%Fz9Y_>ZqVx~2va zm8oH}r(V=$$L_0Jb$;-k({DbM(Zdo$*Q7#Nb#mC-|Jc;|^UVc@2cFz`n@llu*-?f2iT|{QtP^i>n3@4ylhfofxlJBZn#pTwWNK_`VrokK=fr$u_;pyP`DT>SDibvLngoT-PYC-FZK|8vCD+ti2nUx@#cv0E$cWmA9C zKu~D9+BCp~ZqHwd|Bd+5QPUvPU`FBZ#Q*Uhpl~?I8j(!a_qRXuXx6dbt17$p__W>Y zL%(4T)Kr47qm#q-Jk|TL7jwhEJf9hOx%j4nZ3rs@Rcac~7~E>QK30950fVNAtok~e zTz#di7Vny3nieN_YK&ZVl*~WfCEhaIw1km6$28Y8&s1R|(|pqc(?ZiC6S@vAkRTx; zg@k$})F(ko0!IQ*f)1i3=P*yk=AEE3Fh=>7}nu4U*x zK!QRQ;wmTrc+Ah-z4Eo)sJ*K@R=t$6$V2GObnf3#E!AJsD z?_BADCey0``A{;*t5%GD^oKP)hduGtoYN<=yJ9XFpnn5l-%JksY$toWymLKX+|=xj z_TOyp@!TcQA2r2#{Pvj8DkvQKji*0^d`72h;b!_eX|rV-eq`Y{*CmFnoh``2Gv{ zo@e-?f^Cw(*IWA{-;7+mqhORqW$#g&5ts4&8-o(O>)>by&byV(z)mB3oYfV zTwk^cAgmgja;8dfYKl2v&Wib7b7l6m^+#~nLC@q=3H~0Ip17h zE+iqFgb)ciB(x==9SQA8Ky}`cgia)Mt~4jcd(A!Lt!6VD?-jx#ym?})S?FEIZtb&c zX2ZSaViIyicG+-mN?H7WU^f0gaZRAP)Xds)=5glhNysOmAYz_io=5@;xoho_JH^bJ ze&%v?RS8`HZzS~(bXDQ>qDe{Qism=lT*0uNW1efCM?yCex|7f&Y9{l1hHXz0di@8m zy$NV7OGfc$%>!Fbbq^2ObpC@|(w?(E6~p#sguNv>?6ljuet&Lmc;{w8bL^@9OFRAT zam}}zv8f-XM4Io6LE8t={vU@InOB?H(Y@w1=KIX|o7b8jFh6KsXO5Vo=Jh08MM6Ik zib&{B!qp@UAmJJk29hv{gu#{O4RMcaP8{89-o&82R)luQMQBUwp#2x1Wk>g#cam_O z2rWChH^DB&~1LlJy3?-pBVt&)nwsK}%apN8oO0XK8Oia}Hj&f&_|MI$1h1+~4C64lfxeU<-)GzW#M|upSku&|B>d+|Hj?Y z$I>qbuH~v2xQlAYI5F1-T1Em~%OJ~O%e9v4EJG|qEyb2$mf;pumP<%jO2SPfEF)n# z2`fmrnS@(NxRr$4DlMZD;ErwZRjM1{j%VPqgNoR8u)>{e|0RXVYB!tlw(YEQL)W{=97G_9m;_?VJn zwX51N-6l>eol-hte94sRo-%&;j7d|=3#ONsOq_!EKp1cGQ%06e8b7|Me0X_D>q(Mu(|6u@3EDCB2()v8{_rYhFJY|Kf{pi z$}Fr+uD^KkOW(4|@?;!dIPm=>JXphBVno|4FEDtwTb{M-usmmZ-m=rO%d*?D$AVtI z2nkUV)|2oM2^&b*NW#M;JVHVx36EA<_9ftbIsTDrd6mJ-&MAt)yXj(kneC}j$8Ig} z#cp|@v3rb!$3=EOtkLOyZed4rSiZ2J@DjB*3+{2e{S_DP>F~9K?q%`@yhKSFx<~tzBRq)&eW~q+TWAP{i8R+Kq(QNO-f> z;I;O)vU5DFeOM3G>)y$%F-(7aY`hqUk6pXr~)8Q{$t{IpggSQl6$0fsi=C{o2mYwWb zvGL)wh3n?mfAJD{Cs-%P;l;WOK=v-+wf1AR-Wy4^o|tRXti<5G(K_8a!#dMC%R1XS z$2!+K&sssk`y`-R|A2(!Bz#E1Mq?X>-+?~i&Mv0mjd2p3|@9tQ4C%j zo{~laTS+ARQU`DCf9hISF?jDK;d2pQoZY+LdcVG#^?n}RiI_;EWxpxEV*8AGKC!sh zTUley`j8cksIN#k8L>WWMKcQi_Xoyrtu&ggo2)p;BWiuZy4eb^{tXG=l5i?&-D2I! zNc@h3@BafNJ_ovm$*m}YWPEV8YI|+Y~@FxjpNI3Pkjz?X%Z+nuD~xk%>(02fiQGCaa@)L)+uG;W zHW1v}29eC3;MR5>xNUG!;bLOjyrQov_It6f$~MwAnz1{|Rzfl-$y^cJ7~5Eqxk=__ z?AFRRuWf>jwf<}q(QKA^V7n394QMvY(yD7V*J~hJ;f=OgjQHuc8Mc`ugB>*{S(B)3 zwrvh0zA4FYDA1K|Hrp10;zh|6|FLLP%Wt0SY>qs0?>mpaki(8YIbvIius0=#J$O&E zf%8klIn+wsbDH)=wwx z-7MyDna|Ck`iDqX!2Xr(rcw9JTg%_PwrvdF?Ig<<;e8J9+MXAVF_A39wyKK#;;Xk* zEbVr{LAzGi!qS>aaO8!^Arfmz`ZW`!M-tWc9W zMtrq*Z67dv-?P1M!|TIygh`egwH>#8$nec08NMa2IKH0)$uE+TJW;XV{Z-#C`_}bq zR(yW^AwGuhNre46Ic(O)Kb`6{H*e~r=hrOQSkZxvDXDvined(M$2h=0!~pJ6L)MA6 zoVKShfPc6BVf)i|#&*{Bm+hSGyzPQrLb7fo>rS#BB-EMvQB)B#+}0Nc#~*p6mU{{+BxdrhroyVuTI zbM{7dxU~T!yC!09YHvoefh4=O*3h;4?5y!;Z_PaKAm(}Pe&%@xSLb>4of0N!&$hP% zy7rJg$Bs(nI+6_`+0dxHy}biNx0qzZt~|Q-e2L0lkc{K(HoCR_U+De$&JM54u;g_g z!_c*NMc8i1VZZ-r-k846b(clXZa?uxi-u49?Ro7z?R{e4+Iz>q9RYCdepcrVPO9_7 zT)Wyn4B*-a*srk{!9+|B~G+LDGnm~b)z z)yEdw?Dt};**@RCh+(zBzK~=$kgP0XUu<7OvMD5+!Pu>p&MNx~`)!QEo9(yQZzWkd z$)=KQTGW2K{SHRqjU=1?AE5AFkhMCQtQq;!XLyxe?!N8BQ%^sAN6G#eg*e&QUd_qA zoz{%mojSDF;HCBYpZ-3v2Th&Jhz71#ao`X?_m%>W8Y@qZhzLk!~UH8dHYWLE;~NBD@aBpn@_R@BtyY3 zBH3b+q1cy_?50Zl-UP%iHtgQuBp}9uUXm>nA-$EhM`&V*kp1l4Q4$Y-R0n z{GA;mzES)4_8;s&lI(Vp-9fTDqxPTezc7yPBH7*l0glgr&9liI3qv+-%wOMKe$&s! zQQR7RL5$<`2zwzp?2>8A|9E*!IJ)xerXe>L{NDa=jve(JT#RFfG{*5g0MQ{Zj#njf z%BchuL9qSRFQp-QjRJ9WIjHN3#1#ww7cM zkPI0QbBK^EO0xANd#KXkiR0^N9PhbuU{Np0Hi-B>e6i9un7bg(9%qZeyCKL)#vIQp`- z^W)XEon;F}3mo7W4B8#nI0ib<6ZZtkHk0hhsN-74b&U3>NVet5)9x54Q8`8>6FlL9 zCGDiM`)kX~qf0iPS%J^}9Y-8v5O!>G*o@WhE*;skr+l~U>QP+3ZaXfa{dxyG|Bc(@ zm=NRkX~u0|M*HJQw2Qe`?wH58o$8q8xY04)F~c#_G0QRAF~>2NWZOu#on+6FYzN7n zBiZvL+exxrB!frZQ|YKkaJw+UEtd3>Y_G`e{)^ncS;y_a!0k%L?L8!WLF9HdxOJ?N zoo6E1KB|XOV!!aZPl|;daXiGpiaJp9zDTl{B90A?jU;=SWUtg7y^lFq+t2YhqxV3J z-p!2OgVoV{dpq&fo^kA8^lo!(cfjCYB^mlWUyC}Pb3D)JeVt@r;!1Z~Irf3b{mDeG z>7zb!_SwSiOFM23&3UtMT#VkA5%xfG*e=pXXJ$TDpkFcRp^TJCM^L?qea?7q!N%vw>5|%x|kR70{CGGnk)K&CKufB=gfK zCMCFa8k{)QBkD9dO-?h(z9iXKBs&>(TAen=?bjsx=E`&HOp~ab-eeZ}*|QJqxvH1_ z-VtXujA^T9JEtFUHbK~?$zc`irn~Ex_KD7K-g;xt678tJxplU1`eNKVTgA9NRYS&! zxfXDC1h>whGt-&n%yx#HInK7ucFy+B4kY`YWIvD$A1ptS>}QhwLb6{;_8ZAglkE3O zXQw!~&b&CcPMpp zozoe+Q=QYCH5iKkA&}oC$X2Qj&B30d@&I&QIp?ht{ifx%$qpJ^p*Qv)AtW z2uq*9?jnR;oE&z?uy>`?3%cg6UeJE_sSN?F&k%jC^CsttIJ_!so zVofi}bqRLmFrN5FF+`i>}+xZU3?Id@Q+!=Mg z=X{@W>ms@PKfvt?u=q(biyJb&omJeW$E|m+nciw;w^r<6q9aZW20N=641W8@)utEj zDSUrZ-tp0$bIln0y2$NG=eKcg(Y!#Zq%m$^LxqQ|`-kI*oIg4LV%+}h{KfgJ^Ec;d z=kLxxoPRpcIM0$C95yC-6OuP2c{7qXCwU8!wi2GgnJyf?HiJn9r35VS+9nGr`Ox6V&t)w^(;&xUkbp)D>_AU6~}$ zCV7bDIZ;=(E5z7sOY(MCo?TZbiOSVEncbpqHmi%>UA|epDr-nV{b{!|c3pW0o1Yxk zytdOu`$t`V*xu`|qC>BK|J&c}y1KZ!$Jljsi?PdQwfs}YC2BlZU)Qx@*L9VvpR35# z-*vTXfa@C9K-VDGV3Kztd1sP`NuEpcJd)>=yny6|B=17YVx_RIWBA}6LHOT%_DhFlJ|BrhiUFp>`^`3RDataLq>;C6Sz?ykL(#xAVqCHW|k+tF-LS6+reUHOzc zaQ_8x4>NF&ki0|$_icdN;9RPkiH+#i|0DkUx6?iD`j~O`q3a`(k0JTki0g#w6Oxyb z9AMSb)C|{`u5TEIU%5`Yz9#uNl3!2q@ln^eu2T%d2_&ERAHeVzp!I7qT6sr0_KYm- ze&*&aAC^r$@+{^z!F~RYuzw_nedg5h?zivivgj%IQ++pldj_Wsi%nnGS=V`%&wsJ{ zYjUjqy1?o$%uY_iP}AVJx-h|_n|I4&__}2=e9IZW7Z|=bB;6<8qHWoS-rWh7;qKt>Nb&_FUl?(BcB4JD zh~!Ia&F#7i-Q8FuU6|P}W@hW|&dhd6b!O|?C|Y1&cYpBizRKOt4TpOZ$(NCQdDMNi zdjR8o1<7x|^1Qo;NL22j$qW~qPZYeVzLTlHXb3 zJ6f-;L{+a{imqM<_p}O01xn$LVs-K8((xs2+q7wO^^}sbDQ#ws9X@f)jU}aR@+Xa) zI-z7@`II(n&z3f&eTv##Q!*mItQ33Egva0yK7V>^f9s6@x!P~)#EB(kZMsbyJ*oBN z@ugGB#bTV{o+D`$b%o(28z81{_V93}VS zl;VhciF+x@SCM>mMOpmn@|5CrZWz_Q6=lV0WACEyNm-pcLE$(+;5J8%h<4+q%zKNBEZyXa%M^TrRKg8hD4)eye_@Uk%_iD%RhyePI`QpqUM{CZ9rIh<{ugr_&E=IO=p zYs}o@uZQxe6715ysQ))`ydbx*suD0@{>xkSu1k46h#NlCy6@?xOciSTQTC^<=#zlJk`bOq^MzB zRdMQBI-kc`c*RzIU@?{--9J?=vtK=4NvkK+t=+%5PrHA2|Ka}Aea3y({g?Zk z`@H*tN8(BG)brH$NIjf~_Xr-DNA6L08h9FdQawtK%A@vZJX(*=qxTp*Mvuv3_EBfAN5lLxl7B(+uSkxD^tUAcj^sa({3nwCLh|28{yWM4B>7pApCkDNQlyZg zJ}Ed-2&9mcq5&yVNueTzh7>wd7)W74LOqQ;i)ibkYpOo}F?XiAD^q-aix z7NlrNidLlXk)kyz+K?ih6n;`B ziq516lOmTCd8EiEMFA-at5n>c0r84^SFyU!bRTweD=!^B-d7T9)A>dvyeJ&(i1EWG zUVrgIn@})Rm>vq{`$GAlpf8x8pX5*&d7=EAjQpIeP=5X;9@}$mJeEC|7fWVg zc0pF4Fx{6KE-3H?Gjl?|oa~%TUslMUlOIeE2ZHH&m&P(I9?J`t7faY54(9sP^L-iV z*+@%vMvgBhuP_A9lphR*3vxoiK<=fnl*D7%e|fP43&ME?fk2Kg5Y7+#g1Om+zECJD z(-#V62XX?r1^Ib_oJ(UF7mwwo%Znx84;2=MvID+QdZFJJEX+a)rRVy6;ZUeBBP%^G zl${&CG?q#6SPooXEP3hq;Xr{u%a>6YKw7dhGJLr?;eanED>FNk8O+Ed6~JnSs8g5g`xDorLoM2$8zZMV)19?<>rO6 z3w#AEpMvQ***-*)>C4FphJyirI9!-lrS#{g5&q>ep=Y(@Y{!7!cARfz`mlsQRL8uTZ2>Y@M3p0Je085L%FzoY(GSX4@p~8^A z;L=!@#$!2hd9h?>6oj(UQRaC$jF;TpOkX%RT;R(O6y%39GIKLB1KF3xa&tVEw=Of5 zU^tKw42FGy+}vzmFe?C?&dkgAMJeYG{sKA$-i?n2A7UqXQ zOVTe!4b*2QBvetEHk zLV&O!C*VVhGLT~heqXLX6S0H~vI_#~!9cjMN{(%a$MVtT#gbE)l?5Zq_JuM~Wd?Ke z(|zIe5UXLr{)_^DdO;{qrG|Ml9?OZ#izSqsg?bE@oLvA)f?23*a`Q9ue0kweMt(Ru zJ109-B`urdv3z=&v1ElZ@V|8ASO&@kjQDa2kYgDcs9g|$mcME}_H;a!&o3{QY=3T6 z0EGDbK|gXVFPP;EWd$>Qg?ZuJ{9Hs;m|i6D3YuYizP1`UMwR7bIu5527pj{WoSXkFyz`!ln`d3o?PRqCwmhr(!zg+oY7I5#^T7Euta(t`ao9?R*=iv@liP0Oqdcw{&*wEFUWVZWc5 zK)|07^yi~dR;66NipTQD<;4;TXXU2n_)&!g3y@<-0Q?K8O!$>tX63=0%qqN`ipO&1 z@?r^xbF*^8x$wi`e6+^>*||tdZrE2C^k?J;3k&kI!c}63vcvgVnHgy2<)kAmFl**3QI8d%+Q~!t1JO%k(Z^#^ zTwW}h{=D2!b~x-y&r3(Spe9BOHapXo4~LI7Y<4(1ze>4S;;}TmyjZd`!*GxVS-wmZ z3}OMA`1}Gi=>>t@a43Kl&ZQnX%^8nHd3mt}087Z9m+33a&qr;UpO=j`e_k$1KRpyI z$jS)^3#z2W8;?bOd9k2wN5eaq>r2lK!qD(l>ryY4=8MOozr0v7Sc5x&FAIDT!;-^j zTjHA>CXk+yTbS$5%MN8<`jwQH5s$@qd9mcd=c1b;*O!HcH)~s>?*d;mzP#+PA8qsO z5Io$amYf}r#e8|OYln)AFibC``jiV3%|jr*%*3QT0Oav_4fY^h+zMdSO7?HB~PRPQ#3=OCLHk zt+?ui5otJB;nEPJ)5cW2FfQ%-suw1vO{#jKENx2F3)9kWta@Q)+N`P<=B8o6;-%5f zPg_v+!s4_gRWB?{TVD0TEorw_y>Lg`omDTaOvAX%rLn9|TT}JI+O!9%UWlYct6tcU zwz2Ak%CtwTUf7iOMAZvVrERHt;hD5;RWIyFd#>t*U1__kUU(sGU)2jQrM+DB!YgU7 zR=x0g+8b3b97#J`^};)8?^eBVEbW7;7d}e+xax&Z(>|+u;mfqIs$Tde?c1sszEAt1 z>V==vew7&4rTs#RuIti%BSp80CPOg5*L(PclA`jm*f|o__SDD5AJVX>YMn$=QSag$ zm9+ENsiIZv+Bz?~w0lz*P-|09xlk_xZe>lx6yC?6++M7(av0x=G+w=s(Y9qj|w zdC}2+%|FK>QKdLioZgld*A}bW`U_EkXW;t{U3IK0k~OhdH*R4d9bb&0aBe7+SzJ1K z>?9oiQ95Dj_~GS~%8G}VO~?}Wi}3oqZCK3c#2>WIi%$H(Eav|o&pi^$gP_-$NP7ew z{r$ycW_xob#!7Fbqp`<7##V}F~C&dU-j3mV< zQj}Cm8hP{C#20TDZ&z0ol-Eq zbPPMb1%*`}8zs%h5bMa2zQ}~plgcJwsPNLu-KN0MMwXS7BmDpT;hNGi44<-*Satl4 zqWm5MhL@ENpNR8Zsytg9RxMU*;~~RkBgdAOW0ZGlS;?S_H(&g7{EZ5SPbeJ^dp2D9 zKD{tRJU$OU#cI|67GO13I*yuFJ}QW?dxc|91BdfK4KxtoQKZE1{ z@P}IY#o`?1Ey38gcer~%L_Kxw6^`c11NHK*J<)oO(EXxS? z!Z^8S8V3CPm9XLe{-x!tjLb=Y7=Vcru@ioh|I}x4V(>aW>8`v<6DE%@VSg@~I$^@_ zvKjH}tf*v6Y}&=uxl>EWkGdM)h?$w{aq&88N=k9mTkf6eokj}$|7=oF#l`AshL=sS z@Xqwk^5PFSl43e3W<Bso3BPH&}^(5{i-n&qb-R)h;>akJ7%ZK;T)Z<3P=AERczm-z2zLety#lS|Gv%*Sf zP^00@%<*6S8(|{Fz($isu!#**w33v?55jzV7oj#S85Lszfey>${mUjzDz8YXY|v1r zw>ug)Y1QWHMiZxwAAdAesZwjS1A7-G($#1rn*wniCXAHBD~*_1UNQwWNpIbCk0C(YXk|6No% zvjp*sE*w96%oO}L^L*aUP2%~Wk*O>A3IXQY8Z%YKmlVyIQeHCQ>Tc{Ixj1L(NYiG` zTMUlB+(g_sF#fM4>_IF{q(lAAnVozzD(ki7*N+_j6O81mrDg9E$ zq>PIvzg|khk^+f;CjOB!j}#SgKeozy?~vX_y{48AM-2;iHazNG%5T|L-R?o02q;CAt@GZ;HD^C-VNT33Ks^RQ}(hgeHDvIu_U&) zuaU8pRx9 z;h`qYKnH3YzrT$?+daJ8?GLu~XT__Vt+5$S$Wj?(fzqH>kRNIbb%L&f20+(ALnRVJ zIW!Ym0^JI&hSotFpogJK=n3daXbbc-bR7CcA~E6-Mh&Ec43G&zyha=3fLxFV%7B7U z78HW;F2?pyN2oKD3*|$27vlJzJbo8Skw^m!O|bXGb{_Cn<3=A<#y;!=x%5& z^dJ<0)#gP?1nq0lgB z1cdy?XOhILfJ~4X0w-P@gmin6ZZFd9#XEVyiZ=)$o!)`aR0wwBg{?J0JdN5xouOPP zA40s1dO*D(+|vm8(TIgbnj75=-3r038r=z@92%h<8m)yMgd)&h=p6{@Yjj2;Y1{xp zni{(yyjNqqSK~I29|}N-yD{vbG3=mmU#K6{9~uIoOd6w18smK%S3t0X#`i*qr}3lE zKIi~+0{TNDX@dM~k_I(}x?TNGlUWe*wF&Bi zCM%&z2+wcw5%i}-(v*kf5Yp3B38^71g!DAcf>8ENuZFIXNLsuFeF&jUTl@h11pNZx z9a|tzTKol_mq=QotXlSja9>N5bxV{_%i$2x+;Sp>cWXHvnh7m{kp7lPe@mplus2^<=qd>5@*!Qm!4Np} zA#J|N5T5BPho(WucOUZIM-bBK1N*)^p$DNY&`t>PVk)G>cMLiXp80rq;9zX8!BX0jt=z3@ZGzq!^ngUIQ zU`zgm5Z=qb6j}zYfNp_qgYd5Yr=VTXLFirRTL|x)fi!0z-V7(y7-|YNhgw1z5Xw6v z3kpHV>kPb4#u#WWguKo`Tp5Tf194@bJTu;aevwE59OQ*^AfzKO2*P^?kbeR27?=aW zE&>F>E&{NN0G=1P2|`{5)H@*uGhug`hafy7^G}H+3-@IqFR}y(?~rAMU{_g9pk`1Ds1<~F z$U<3V-47xCSxA2t(x3Gx^f>ec^dtnk$%5UW87Ij?8D{+|k!0&3@S5EYx(XT!6+^?I z5zr0LDrg;q`?GgKZ$TeIA3?~o?C&7tSvK-48+n$Ev}YsjA-s18?;UCYA?+cgJ%qG} zkd6@SBh&#x+C%-JArRsX4TnZTCD23&+=R-YatL`8nhvdmU@xI}pdTQ_7rG#kA_(@D zvj|!N!ESSq?>Q*@oV%d4&_?JH=o#o`=pghe^cwUAbQn4cy$!t!orJ!DPC?&8KSDo4 zze1-WIo!V=*=!`x?Ba@5 z__y7hIfqjMBLHHQPGYPv3H*@$b<2{09hLnU8I_e$*~BCz1*wop+4O8>2fNtI0SDp z6r>2nC`lR0QIRTCrxtZ-Kx3NGk~Xxb6J6;+Z~8HiAq*pmQH*5*laY0~g)C+X-vmK< zJ(tgfKFen#CttCY|Ni<4RtG@^f4xEh^j@JbVc1s%xmB>E3a@z=1QmPY)+!E14;A%L z=|k+PQbOccNq&|5^-7bN!Zc#;!rKY{qGEN4{^)JcWC)yY9F^5E~Ov!0D?#te1q z(VDinrMhmZu0HGPr|uJ;Vn6k|Fc7~V^@gJNdUCGs{_Dp=U-jeChe$?XuKHtxpn<>M z;8XP7K;I4Y*1$Xs<}i;1EDnN(Nk~TqWZy6=W@)I;hTGV|t{`YsfYO9hj*7U6M!#^9 zUpX5Djl=NQ8dt_`G?q!@=Vp3twg&w+({HmMaI?+rzj<*=qW9)yS<7~IVy@M z>FUjHYIUnmLmH#+ZfE!dd+YX+H~bp}-9zZHyFGP}%_qbs5n0GiPVB0?U3Je-L5dJY z2})6h7T7`eWyrhxy&&ij5Bu#=3pw_fjQM-aV2aB>n5XB5e2yM_>aC~Vdg`sGo%Rf;JoeGED%Bal8syvaEOy(|-g>?W0ucti?4eg2 zlHf*q6`&Ai>?NCCwQ<9}?6#M^_G(FM^fUpx%J#%w`@7S&9q?$Z&w24bc05`{-|gyBeskf$nOcyBZjm z1SBRY$tjID2Y$sO+`&LQ8E7X1?PA~s)Ej92f#w}() z>JOd8V%A{~L%lQf7#C4*=p)=gg#067VGj}NMaVzmQ{*3E_6Rda6vS*1)v>3Dx-_6M z&5%_@8`{%}VMH+!*+q4D=T<2e%rrjq|vnVV~hHhP7inzL8-^xP_jF{moOJ zV;{rqVR&rxKRhn+v6JCx$bj62n_>7D)&WheVN zh`fgX%&(l~0+;Zc6e*iX^&{;k(q1CF5s7yqCt==5^&&UoRwIAFe37z`JcZwtNEt<{ z7kPzim^<=K5JZ_RDiQLDl24RfMVT`y9T~|&c5>kkqI_3T6{v*#qU0APzbLau)uSOY zjB=|{gD`W{ZoE0-L$XqpF34}hDmJl&ZRmf5yBcu}{f{__o%kPp1tZRJgFD>ke>~wC zFL@mVBU4d?@>HWHdK+0EcQw-fMz*0n9hr{(k8}$o?PR2#jEYMFQji(-Mis-%qs%+X zyrbOfs79D`RCClF)q~y)XB1;GN9%dCo=3}abQ1Dn_R)Ru=I9@|9t30D(wGd$a!d>CV2murbf+hS z7=eezxZyDqnZh(?V1_a3jroSvtYZW68?%j_?BX}x2Eo`^=xwat#wI2?DM`)ee1Tra zmY^IJsX}$!)7XYI!H&n;@!0XGKh}Q6+RNB~f?(W7cxPNf(xBeB0+hxbjFa0qH#e>p zZed(~)E(CW`x&R^xJk%qoEgW>VlE3<%u<%)R>$pT9|t(dPaNeqavXPxGo0fBPlI55 zA`0To@k3e0Ve~&i9uv}$om}K0A7PY5{}b$FLS@V_!CofV#{|2W(1!MOp*y|kgRCZO zVL$qtpuY+FoA4`on{Xb#2NUjapBK2Ti6Np91DQ>f*~G*oB{}vvQT>T-VB#XyvIp-> z{2B93RBxj1a^h>=2f?HdFyo}y#33H)PLlnkoTxd;oRcbG#!1zvNge9bh^DyJNwS(W z05>{m2oXdwk}-^9B9oEjq;E0zq{l%p*=ISBs;1q6P>N(6k z_3t2diF!OqtD;*-V+ul-bNl*vCwn%~W@$%w}#x z&6!6yh8bu6%2_UOi7WiUO3K5MM#72%^#UlZU$c?$b>djZ|!0u*!NILXB zs~PRFi&=V~)tv!EVjr{YW0rl)(*G>|&(i-a{m=4SIm-;QR^j(x)_P<;Yb)NF^#d2t z@9da-LVWZ!+x}*!AQf(6wmr_ygWHS#ao=U4?+YLCA_x`**vBIKSY#iI^uI{|i|l2Q`(32} zMH$J0>=xxFAF^9ii@tbg(HMM3izcC`MPFfmi{`S5wQR>-EjqvJ`!r^Vh`Z1%--P;c>ie!zT-WwZDvj`0hpPPRJ6UG#WvkhPIhP&8jLUxJ7v!|e&X$>XnckND&fh%cIWKw5yCC?+t$riNZ}j|4 zOkyL;Z?ae3 z8d$y(^DO^~d+2fb8~zP~6(K%CZ!13L6FwsiIq==A_!76ZA`ClTQI3jK!Jb$2NBtG{ zv%+3h{J}qXXT|#a9#ic5-6Al_e;RJ6P#HSE{$teXf+-N^`I5irH3BvBN+R&2$3`TF?M&iD{oq+rLb{aF-#iJltZ^!HHWWAlNFGNwosfl{)+hgYS z=3Q^z^}`s+XvU%L`gts54I9{u8Q1T`JL~u27S_vt{S}^I&h;-baon?1r8<&SW-v-?)I~ ztY;Hj*oOW$>VM-N_VEjH+jx$Pm}TQ1$Zq4UAlMWW{cg&OzBcJ=Qwd7rwl-Bk51VRH zhoRWlCVScR8+zUJinl?qIXVeZZ?pL~7oZ5mFz4p7$b54p)ZN^eX2@%EN8IRUH?TPZ zH?Ub=o87@?vu~b+nK!S-Y@21X`5=e+8FOyd(`LVSo6n=S&A;a=Gju87PO`v9q7p*hM@l~!->M(ZJEX_=3<5| zOZbMBm}kpz?xDvmdUJ-s_j>#OBR(NMiTIRcl%hB8;roTGVK@6Y%6ZiL{vopY{@)n7|L*DxKqzN^}KT&6Ig}WcRmP$ zAH4ZP9_llgdF;S_{a^<_+~5{>c#K>7;Vt^#rT<-Scvo~{keK9{VOJV5keO`cKu){5 zFpvl$(c3P4?b6pS_qA&(i&=`_pj}(h&#qnUg`^RZ+-VRcH$0pAK)PNv0L5U7cuYd|7yMqf;|Cd+#{zwv4}%F{08nx zLNaoao4mNsJzr9oFiIfDJ>it20&Ow(9y#oBe|sJU!Ct%DTL?Mq)%#wzx_1F~u~+YV zzu{Z9Vjp|$W3PSe)&E}o@74ca{qH@`Wv+6a+uY+J-r48Q_T{1|B`Jfx_EpCI_PMWp zwP;Bj+}1ufwa;zrli5C*?Hh}{_Q`ACH1xVpKl{|*Z#VnxWPe7?v)?=W!>NgS``gol z-nh^Gqfl?Zdi!O+e>z_=n-zS=5sq;JdG9~V1uk)gKe&lm_rKxaAh4F;zz0Ml2C;!4j0FELEsZE$ZTK4h~2C zgJ<|1xgUIscMiqGyob~~lpWa|Dn?1nc&GxEa07?bJ=BcOsCmeohel(@Llc?8biQH^ z^I3$f4!O@m+u4bmIJ6gG96HEP97UFge&K!){1l6vc=M;;%*A*4lm35_<>6$cMHYuM zla;&_MgNEGaM)fB+s9$MINXS)w4yB?=)@$}u#G+JM{kD@?;MSe zTR1BFqvl;RPHNI3yJMM<(Xn>) zW&nfH*D*aE)6=oBjAt$jSj|TCa%?L**vF5!rDGO%%q<;z8U)AHKW;b2?c})JkK4m> z?;JP#@o}hkd@<|T#1`DZ@m=h}E{>~v{4{1g{$I@(yyjgH{34@YKI9`}5r=prBs1B_ zK`wHWk1vtoFGX;xzm%X9%^1oOy!p$WAUN>}dC>of?o7n@bz%7?)fWM!&RliJwb$@QG%B5vj6 zOWp**sc0lXy;J5tm7l_dVa`+Gl&2!I$^$3ZuFE}JLNu4dFRwHBAJR=Pnq$Q z8Bdw(pKD^N>gUjSNq@(^Gmr^8tO&Q6cSot>83=_$-)HuG43+dcg| z_t4*I{hij|=~w9O^uIxH#*LkchnqT+1~+{sD>?WA_jJaN&xBC|xt+1kGwPpl1806h zku$gP&Y35e_pEwnlaLNsoy~?B&*nvLXA7e4SvPRDE^3}N=h+^Z@oYZ^GK677;d?#n zdp#?wvtQ%)rhub;3k?)Y@*=?NVpCCAw6mOnu$_Q4X|8ufD{{bH(i}MLc z#Ajqg|L5)Ge0I!m-d@hz$9cOrA5M9yP@P)Tp+7TO!YbCHxAU8DU*~_oeVyNr@A3S@ zAh?hkJHB8i7wqIh2RhS-NYuM98#7-p?*;Q-*uWOdd0_|YUig_`xWqMXV8#pg`R|>- za0?e@fAJI2Va|(LFyqBs) zZ=Yg!zqLTFzb!(~za8W-djIV>=eWvs{^Sn&|4skDJ;ME7(*GsDkC#5;W8xyaOR~F^ zo7#BiQd{iul3iZX)1_YYMMjrKGmcrft4m+A3^#S@TQ>3?Tanu({ajZ6vfW&^lgpKG zPnW%O+3c6y?Pc{YPh>9UySxOMU0%&v)}!v_y_og#e>Kf`*^HOXc=>-k<{zH(inqKE zg5ML9l;or&6`zwH8UCJ`Y~&ypmFdJ}y!rcCUI)RI)ad_;46h6%icySZJTq9pVwSR; zRjgqhX1F4^EBkQ2SA0iTj&XutIfLG>+SOJ4UDelBeO=8-Zrs(?BIx02NlGK1s|(q~ zVf1>{POjd^F0KWrckNTmd(FLG%Z@p(<>N~Vq3*ScRG}fwXh|Dnbj>^0y3vDSOhiW4 zWOQvMX1pf*YxZ^RJGQceUF=0)*DmlIm$||p+~hWQd4L?R`PTl3gFgT8`|w9kzQXSQ zFwh_W2Ep}I==pjX%Axn`m8nBhnxp^gt#QlO+tUZRT_1uOu8(94<1x>5w|4zFdc1xW zH+J2PUB8Fku0P^2?}OmR2gJi(ZzSh4(vXR)uZkD1NX1m#rj&z|rzSEn&)0+brOazh4VlMM> zFETnHZnr98mRmKE-K}~IM8CI|p|4x|y5+8JxvN_{*^M4<{m3C6 z1;K6mx@|AFE1}oho#;wGMxfqp^WR>u|XS&f7H+o0UckJMf z{O;KKoq6c-&K2GV!QJ%O+g`|1pV4OMVJclB%e8 zzcoE@r}qb7&iliOVifA$m)HHd%x4wf;s)-!f&1RMzY}@g-^&Ru^O%2ljv4RE=)QeD z_<-n`_krFX#KjID__iKoAsac6;R6{ykl_P8KPW;Na(vJjvp-nGKKw2`j7=`g^{@-$ zn9f(sVIIp^$9nYta5GzQcMpH$2*)tP!!w*mb`Q<-@ZTW#UrO}&KfV1=Z~xQV{|Zrp z(v+nFmFUfKe&Qs*^A}He69kW9qTVCAQCIUjAnjE}xYPLF2hRn$VUGbfz2nf2{w<`hTqd$1;6vhQ||`!gOXM zyT{&nyqE*%_whsY_4qmN>+#zl`1?cj@OLcY5Er-ew|)IRo*8`2cWh-Z$5HR^8_4JH zr_%tR7klWLwBqtr& z$weOWV^2>DQ;y2$|EcVr)}tZvd)kah7O@t6J#|}8-PTh*J(bPVLmc54zwkT=o@JpT zwP;Q+`Vzqe)O)ss^~mO#IiKxd55Dhb?(3Pl&(3lI-}|$BJVa*Cp7P&2uXqy#&;1rW zH|z5pd_i6cAfx9+DNZTEDUa{>E%#{A)}X5nSpzMxso-wiEKSUd_UtzB1pdwaDz%cYM!w)O~dnv%dPTrWs$E@s$~0z2G(Pg24X?UVq3(#3D6m zNl!*HlZ~9n@O2*Y^Cg9-OCRRo&DU3h;EnI}O&0Y3Muu-jFp()tXC@0-f&Smv$(wbU z;f=k#*^OPik=vUixZgLYIKw%f1i{-Rq(gsi_4ihPZ{62hy}d1nZ|H3WD$@XW_10az zZA%Aa_Eu(Z`!aw**ymgI-@1Xf?}Ol79Ma&OciA!TJN4dGqcJULjTzr{rYk*A_nqwD zO-9Xk=6tsrGrrrvX1-@TKd^`W$m-p1T;>YbxWO&%a-aY47qa{}F6REXCT`y2xulM%%{uOU{7leX1Bp@{z$wGE=QGh}er8p(&$b5Ejh|}ES zE`RfH5DKXmN=#BRkd+*KL4LlZFzSXXQJ*e!rx$%0z+fVXWF%u4&tjJH4J%m1TGq3X z&3w-`c5t3&LFj|zc=LnijAAuMxD$jvjK(L#ClN`olMgeKjhy5r9|b5#I2EZvb?RW2 z4;#^hVJyHqAL{GF@7Ri-KHSHT{KQd?@iYiU%S3r<(3BqZW(ea@FPizItz#2gFlV$~ z>}5adMw3glbKJmu(eCp)-Hg#?6umHEl%OMj;`nEed*6k%pUy$-ux&PWoXZ6mU93*_~-<`a)#e=TOaBFqkBBS z3?Kc)+aMGpgxq4pAP(_JNMiC(hnDCshW=vcFGf%F7NZ|-EXEl07sE}*Si};(!9B%r zPciH`#unrj<601knSi{wqnLIPvoXyuOUyo~7t@W#oXrAc6>}*oS&iIcsvC1RKl5MB z8<;cZUCbEs5r6X(ImLXXhj$|Zyk3Ej*%x59`k8LNhmm#Ovo7u)r%n(~{u@7+s^Td|V$Fb4l z$9nr%Zy)RJ4KQOIw;IPgaoW-z*~b~d9LyPK5oU}d_c(HjvxaZkh`Ws=`#AcFbAnTx z<}4Su#1-Ti=LUarI|zO9A^QBJ937a5-F@%MF@m8@3S;hMm@5D2Iy!)sZKR{OT6Ox2vq(W}-?IXUr@$+Hc_-e+N zQG6N2H)H&kw4ptn=!)Nx_`cWpW0}AtrZA0}%w`_GwfKvXV|=$7e;?*fAcq8QB|$~( zF2M}skU;MVd}j$>U>6DWp5T2DN*I#_*hfP9NN67k^`B7x3GE?a9`aKV-%!F5$Sq+w z`cF6#y(OH*T=bPtPYLyua0T|4a2q?3Q$jbDP%jDXG2sQSa-Bc9gS-;iWg_(xx!*+H z7|dk6lgR9eR-#^_-TcH6&T)w={J|~MOC+a64|x-W5}P%#*%E(FdNPp}b0+ppCC*C$ z3Q-i_RpQ#nEpdI^XW}L_$89EVLwh>XnX%|C@iG1vgpwq{-jc{W$sp_}iEk^(UJh`G z!}!LMxZNbbbB!D5KZ*X6*hiA*=s(H7LFm&E9}$Z<#3c{a@Xn|D`qbV&?SP&>?L}V( zFqomp<LAnF-3AdBMlkIOjfc}gkn^}UQ^V;O{Hi=OWM$$PITcb)K6hQDeNW1 zzdmZ~oLPt_2&oT@4Ik*YiHIF%Vv4Pq$6F;A++96*n$&T@fETtjcEvlTij2&nD{Qz<)s96lFm-j*+n}2rc*CnB<4*w z1GA=^%Obu;Ch62ow}ovSCKqljQ))2 zQ2Lal#=g>LK=$dgp|A9%38x$ts7y6#;$G9&LyqYi(I0)L-^uTR{}_nfWvE0)@uTW zX4F&0CNxJz8QoLH-VA39x0JDJR$sRim~ z>cK6nsW@a#0jF zm9;DtsEoX_>N{%#8q<`~sGrsSX1&2*K`7e?cqdz2QlehAJmjM!YGzY2o0{2bpl&vG zv(={^=FK*i2~5U}*=8~u8D*1wHuGllZDm`*5A0zd2k?z$JIv4c&a$0Aj@iy&580js zq3j7LfH$)bW(kM*GYI+fw?jETCo4Jl0{!PGLK*a*Lq<6&Vul=5X@vXD(TcWoLVh{q z=g;pB<&ag5&B!{3{&MIqhyHS$`;$jhucHa>gSei77^R zrZN}1$hm`Em?fvWIn~Q~muI}@T@cC@5SA&$ojg*v-$J#GU0cL%vH~ z;SbD{?|l%;pAJ3d&rd;$Py)T>52qY;aZ~x*Vz2qT(~G{ir~Jc+ViaT8i24QWr+~c_ zNKOvCQ=ky$EudZjH(HzE? z?-VXV7=Akoo3*ff3d^T(Z~8HiAq*o5xfk{uQrNduSYL&gv7D8xW*r;Ygr5DG;Gx3X z*~taWUL*zHEYgC}tl=m!EE*jXLl*R3GzYmbL(#mH!0i?-PbI1&yP~oy zTAx15M$Sd`S5$vR^;dKYdMoO_iu#6%{>(32;yQnFhx`1E9T$DUYvdMYpJD2Uxq+}q zCbIE8ZHp7T0_6Jsid^itD}jNiK37 z`zY=^D}Ij$n4!2CirYnr5YdQ19O99X#F(K(9rRbC9UakI34N95LqF`W#2CgSs}hUQ zPYJs$v6A&{Vhh`lSqb|rseVa!T(UUsx1?{nq<2c1zhr;ZD>;dIEM_UoS%W($X&)uk zEx8Z#mi(`#j7rL=q!~*-;%}bv0@;^*7lcX$Bq13o_>9z~B?FnsN)F^$%B_~Fin&Y4 zq10y1V|S%LM-HX+UV1Piv5V4rFFld3ScH9)wvW>GQCk0{^%IK*~9tu&E;*`Q&mC<7v@0Hoh5zb*BW$dGjJ%oo)FFYw`4L4_a z4!$5iUs4!#!z*F#@EW+&@K&^?16}D(FZwV7y9t+1_$t;S_i*`yZ($od*^Ruyf5dHu zU*-zexXz#4!JUTdIb4SR?DSChlOR+!9%e6F3vZU4!ye>b&RpeWQLY%}s7Mv6QJ)s* zzg!#KaXB-T>qLKsFpMZWGFGqwd6!qO{AtWw-n`|_TmC+O;aMcoR~iHTdS;7%)~!i*I%;++cFkX;2? zRj5Ey%vqr|W~|VWE_A0CeHp-D^i^R7?z6&d=CXjrEJ4o|^jtxf71nSVvsa9bH!Iel zAM@COEGs_44l2o_QZzmy9?3{aYSNN{Oyr{w`ma=+GL%Dhl`12nN)wpFLcT^{mGo4} zeO3CF4eVh*Cpd>*DqZF(x44U2s^pd`xuwd&8%qSGOQuP8ZxYrmJGPl8gi^rmNtyV zEHzH>Gzis9MnUS)n;{G%ijmk!%~{Mr|1}q|h_A7anw$9^Gt}J8K4e$ZJT-3wp<2<= zW38m5AQkDzhunIK9S`r+PKkMI zt5>@?zLVO%_1f;Vwi#n9NkBGn3iOVpzk)L8zqA4wqccXr&*JvVhS;P{|*=RLuky9ge8|~!)ZnKg38eQNL z*ZA+9Kap3XXF;g3SsR&oDo1Et&W@zH(n%u+fHj!JCr@Z1V z?}Jd&5UD9eHT2h1e@*q*)O|H=No(5DhyIMjT{U%AO{X!F`PgyOC47V2n%ZYG^_#hY zX4xr3WxUg@KIUzvUb8`rK~~MCV8&*%kz2C`sN2j9G~3C4HO<-V9A<2GnX6pqPwsG^ z{{^AuvT7ck7{uaZ;*tQj(>w{ukY)4FC`JgZ^8{vPFA(Ad41mti=E# z8P6o9GJ{#nVIC`4hum6hW*cT{u^YG7;&)`x(mO5n)-o|k(NoJbq{WT3%!`|9S(=Je zp*ppYSIZ_erxkYFQa>%#Z}|sqpyl%*)XE-Od8d`xTctz2R)r{w`C3(_26d@VBh+ox z8MC%hvy~ZJnX#1_Tgj-^0v5BB<*Z^Y?y}X79O5uXIgSil{mL26bCFBD2tuttr3l_^ zJsfw``e*dtCMFrlMIQ3=B_*hU{@d6|8~4@53~lVCjeWGSi#Bp=(~0i%qA&e%yKS~{ z5dF2$UmN|kIg8%f{DvEAbD#h5ItaCO(`};>gV@Ndt<2gcB?X^hpKaA|>jv5`VLkit zPTOBFZ(H@+KIUBzYWD%rF=M+=h(|)yZ72J7xlyy7IonmnjO}Vthx#9on+og=AC5JDL#q#l;p_0QyTJ9kirzD7$qr#3_F#l5>=>1XU4N0Z+3bb zggU#U&c)Gx=ON6+_tkkht60N#>_Y#Y?WFSo%+T3hI@?EQyXfq8J747{x4FlIAk@X3 zb;*vLyXdcr{<`R|i~H(QnW|K$87=9CZ>Gxt+*OwdWY$GyT_!M@sn};1^}D)(u1QEo z0ld?-6z1)!Ue`ACq#pw@W7kMVFdB8c%D(G2sM*z=UH4+fu7^0nF;4I+XSsl^y8gxA z{KGR|@`iW34?^8OK$hJ;B0Z&Phc~;e<228MP!aE*V1P_J}&vsVY) zQLoMDzt`g+)cX^BU%iv_8L7!kZuH;VPI`Zd8G74GZ~N$N7rotX?^@KS5lv~1JL|m| zIrr9IZ~gVwUvKx-doTO>k<*;x2ELiz4{=w$pCGf|GVA?52=)1ZXxL{T_4~MiKI53h zYP{3ud(7KMy*?MY#eM#V8T&lr1+P)Juk8CKL(RVC?3)iW_AN{pB`8f?`+?t2-x(Jw7_*RL~j?WgyCKXDSf=%@F77x;sF*hfG6=w~1O z^xsea{q3QDEaDK4#3UsJpP}#mP0(Ba9`vRkdg^a){eAoWM>CEI$fp18AT%Hb@o~EY z>|=m^46ug*6;W@1y$>+!fS#ChK!1iX4EH)f-2v12irIX_YSysBm3@W1oZ7AN(Q+4GH*! z&+*QX9GG{AdPAxsuOTgIgBgc(p&POrqVA9=rXa5&<{jcrhsEuBL=aEPg3k_co}L?lUmfFB^~ID zT!!~#0E3Aj5_=rJnd?C)@?#RCW~A>t@-yTRX&;gK_!4&&IhJWGVj0VE$C2`kG+(58 zkz4qg|LVR7LQ(pTiq1#GBnf@!&me{}9Q{O%W*ifl!gSnEl>3RAk9VV%@D1LLl1Y?I zqGS>!lPH-)?Z7QW$tFrRQL>4WO_Xe+PVg&dxxgi^@CP?>FH!e+h`Wh;!ZTj-hJS<5 zh^DllHSO>{jp#ymdeIlV9%0ub?0SS6Puz)jCVFS${vb3d5uf9oN#2>1nSqSPJCnRK$*oPg#zVX_$vcz&2||<0 zQVZ`)_Ri#nxVOpM@y=xLOx_!WrX(OW-kIW^DH-X{NW3$}J5wfbh5LACig%{`9fYQq zp$6WW>Yb_eSTdY>NL*g9L%cBs_ebG9B-_;g6Yg) zCbPK<->&XvE(`Fcs)am4pGO(6glBmU=dPT)dWBc%&AO5bJCn9b19mW<}{TX(VR4=S=@={ zq&d0IG#|}Lb6P}$=A=0dS&rtUIX%zIXil2b8rGpXX-=bjf##$+*-iQx%}I0GR+KRY z&7nDVXJ0gj<~WdXXb#PB6eplLG{-4SM002kJB~}x?C+42VaIU;nnQEgaomCC&>VIg z52HCWhaE=`&7nE$IG#asXbwA$m(U!V!;a%!G>7J}gI3I!LtU23pJ|4|kbGGAr7Mio>Y{z*DTC?5etC+?$+{#0=NMvfBU>Q%N zDcfOQ$%|;pn(|uoseRR;*eGTuS zyOBSOvbQ&fa5dL4hj}c-JL@gwNu0m8oE5A_M^8s@4X^Vi?!V`ojdM2k-ki_Hc<+tx zH`g;A_tD&p{Wo*DAMc=9z#; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_1.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_1.xcscheme new file mode 100644 index 0000000..c311b59 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_1.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_2.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_2.xcscheme new file mode 100644 index 0000000..05010b3 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_2.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_3.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_3.xcscheme new file mode 100644 index 0000000..403f333 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_3.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib.xcscheme new file mode 100644 index 0000000..3449c16 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib.xcscheme @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_tvOS.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_tvOS.xcscheme new file mode 100644 index 0000000..8275005 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_tvOS.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner-nodebug.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner-nodebug.xcscheme new file mode 100644 index 0000000..e3d3102 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner-nodebug.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme new file mode 100644 index 0000000..ea31835 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme new file mode 100644 index 0000000..e556552 --- /dev/null +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.h b/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.h new file mode 100644 index 0000000..52ca814 --- /dev/null +++ b/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.h @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXCElementSnapshotWrapper (Helpers) + +/** + Returns an array of descendants matching given type + + @param type requested descendant type + @return an array of descendants matching given type + */ +- (NSArray> *)fb_descendantsMatchingType:(XCUIElementType)type; + +/** + Returns first (going up element tree) parent that matches given type. If non found returns nil. + + @param type requested parent type + @return parent element matching given type + */ +- (nullable id)fb_parentMatchingType:(XCUIElementType)type; + +/** + Returns first (going up element tree) parent that matches one of given types. If non found returns nil. + + @param types possible parent types + @return parent element matching one of given types + */ +- (nullable id)fb_parentMatchingOneOfTypes:(NSArray *)types; + +/** + Returns first (going up element tree) visible parent that matches one of given types and has more than one child. If non found returns nil. + + @param types possible parent types + @param filter will filter results even further after matching one of given types + @return parent element matching one of given types and satisfying filter condition + */ +- (nullable id)fb_parentMatchingOneOfTypes:(NSArray *)types filter:(BOOL(^)(id snapshot))filter; + +/** + Retrieves the list of all element ancestors in the snapshot hierarchy. + + @return the list of element ancestors or an empty list if the snapshot has no parent. + */ +- (NSArray> *)fb_ancestors; + +/** + Returns value for given accessibility property identifier. + + @param attribute attribute's accessibility identifier. Can be one of + `XC_kAXXCAttribute`-prefixed attribute names. + @param error Error instance in case of a failure + @return value for given accessibility property identifier or nil in case of failure + */ +- (nullable id)fb_attributeValue:(NSString *)attribute + error:(NSError **)error; + +/** + Method used to determine whether given element matches receiver by comparing it's parameters except frame. + + @param snapshot element's snapshot to compare against + @return YES, if they match otherwise NO + */ +- (BOOL)fb_framelessFuzzyMatchesElement:(id)snapshot; + +/** + Returns an array of descendants cell snapshots + + @return an array of descendants cell snapshots + */ +- (NSArray> *)fb_descendantsCellSnapshots; + +/** + Returns itself if it is either XCUIElementTypeIcon or XCUIElementTypeCell. Otherwise, returns first (going up element tree) parent that matches cell (XCUIElementTypeCell or XCUIElementTypeIcon). If non found returns nil. + + @return parent element matching either XCUIElementTypeIcon or XCUIElementTypeCell. + */ +- (nullable id)fb_parentCellSnapshot; + +/**! Human-readable snapshot description */ +- (NSString *)fb_description; + +/** + Wrapper for Apple's hitpoint, thats resolves few known issues + + @return Element's hitpoint if exists nil otherwise + */ +- (nullable NSValue *)fb_hitPoint; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.m b/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.m new file mode 100644 index 0000000..32166a2 --- /dev/null +++ b/WebDriverAgentLib/Categories/FBXCElementSnapshotWrapper+Helpers.m @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper+Helpers.h" + +#import "FBFindElementCommands.h" +#import "FBErrorBuilder.h" +#import "FBRunLoopSpinner.h" +#import "FBLogger.h" +#import "FBXCElementSnapshot.h" +#import "FBXCTestDaemonsProxy.h" +#import "FBXCAXClientProxy.h" +#import "XCTestDriver.h" +#import "XCTestPrivateSymbols.h" +#import "XCUIElement.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIHitPointResult.h" + +#define ATTRIBUTE_FETCH_WARN_TIME_LIMIT 0.05 + +inline static BOOL isSnapshotTypeAmongstGivenTypes(id snapshot, + NSArray *types); + +@implementation FBXCElementSnapshotWrapper (Helpers) + +- (NSString *)fb_description +{ + NSString *result = [NSString stringWithFormat:@"%@", self.wdType]; + if (nil != self.wdName) { + result = [NSString stringWithFormat:@"%@ (%@)", result, self.wdName]; + } + return result; +} + +- (NSArray> *)fb_descendantsMatchingType:(XCUIElementType)type +{ + return [self descendantsByFilteringWithBlock:^BOOL(id snapshot) { + return snapshot.elementType == type; + }]; +} + +- (id)fb_parentMatchingType:(XCUIElementType)type +{ + NSArray *acceptedParents = @[@(type)]; + return [self fb_parentMatchingOneOfTypes:acceptedParents]; +} + +- (id)fb_parentMatchingOneOfTypes:(NSArray *)types +{ + return [self fb_parentMatchingOneOfTypes:types filter:^(id snapshot) { + return YES; + }]; +} + +- (id)fb_parentMatchingOneOfTypes:(NSArray *)types + filter:(BOOL(^)(id snapshot))filter +{ + id snapshot = self.parent; + while (snapshot && !(isSnapshotTypeAmongstGivenTypes(snapshot, types) && filter(snapshot))) { + snapshot = snapshot.parent; + } + return snapshot; +} + +- (id)fb_attributeValue:(NSString *)attribute + error:(NSError **)error +{ + NSDate *start = [NSDate date]; + NSDictionary *result = [FBXCAXClientProxy.sharedClient attributesForElement:[self accessibilityElement] + attributes:@[attribute] + error:error]; + NSTimeInterval elapsed = ABS([start timeIntervalSinceNow]); + if (elapsed > ATTRIBUTE_FETCH_WARN_TIME_LIMIT) { + NSLog(@"! Fetching of %@ value for %@ took %@s", attribute, self.fb_description, @(elapsed)); + } + return [result objectForKey:attribute]; +} + +inline static BOOL areValuesEqual(id value1, id value2); + +inline static BOOL areValuesEqualOrBlank(id value1, id value2); + +inline static BOOL isNilOrEmpty(id value); + +- (BOOL)fb_framelessFuzzyMatchesElement:(id)snapshot +{ + // Pure payload-based comparison sometimes yield false negatives, therefore relying on it only if all of the identifying properties are blank + if (isNilOrEmpty(self.identifier) + && isNilOrEmpty(self.title) + && isNilOrEmpty(self.label) + && isNilOrEmpty(self.value) + && isNilOrEmpty(self.placeholderValue)) { + return [self.wdUID isEqualToString:([FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdUID ?: @"")]; + } + + // Sometimes value and placeholderValue of a correct match from different snapshots are not the same (one is nil and one is a blank string) + // Therefore taking it into account when comparing + return self.elementType == snapshot.elementType && + areValuesEqual(self.identifier, snapshot.identifier) && + areValuesEqual(self.title, snapshot.title) && + areValuesEqual(self.label, snapshot.label) && + areValuesEqualOrBlank(self.value, snapshot.value) && + areValuesEqualOrBlank(self.placeholderValue, snapshot.placeholderValue); +} + +- (NSArray> *)fb_descendantsCellSnapshots +{ + NSArray> *cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeCell]; + + if (cellSnapshots.count == 0) { + // For the home screen, cells are actually of type XCUIElementTypeIcon + cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeIcon]; + } + + if (cellSnapshots.count == 0) { + // In some cases XCTest will not report Cell Views. In that case grab all descendants and try to figure out scroll directon from them. + cellSnapshots = self._allDescendants; + } + + return cellSnapshots; +} + +- (NSArray> *)fb_ancestors +{ + NSMutableArray> *ancestors = [NSMutableArray array]; + id parent = self.parent; + while (parent) { + [ancestors addObject:parent]; + parent = parent.parent; + } + return ancestors.copy; +} + +- (id)fb_parentCellSnapshot +{ + id targetCellSnapshot = self.snapshot; + // XCUIElementTypeIcon is the cell type for homescreen icons + NSArray *acceptableElementTypes = @[ + @(XCUIElementTypeCell), + @(XCUIElementTypeIcon), + ]; + if (self.elementType != XCUIElementTypeCell && self.elementType != XCUIElementTypeIcon) { + targetCellSnapshot = [self fb_parentMatchingOneOfTypes:acceptableElementTypes]; + } + return targetCellSnapshot; +} + +- (NSValue *)fb_hitPoint +{ + NSError *error; + XCUIHitPointResult *result = [self hitPoint:&error]; + if (nil != error) { + [FBLogger logFmt:@"Failed to fetch hit point for %@ - %@", self.fb_description, error.localizedDescription]; + return nil; + } + return [NSValue valueWithCGPoint:result.hitPoint]; +} + +@end + +inline static BOOL isSnapshotTypeAmongstGivenTypes(id snapshot, NSArray *types) +{ + for (NSUInteger i = 0; i < types.count; i++) { + if([@(snapshot.elementType) isEqual: types[i]] || [types[i] isEqual: @(XCUIElementTypeAny)]){ + return YES; + } + } + return NO; +} + +inline static BOOL areValuesEqual(id value1, id value2) +{ + return value1 == value2 || [value1 isEqual:value2]; +} + +inline static BOOL areValuesEqualOrBlank(id value1, id value2) +{ + return areValuesEqual(value1, value2) || (isNilOrEmpty(value1) && isNilOrEmpty(value2)); +} + +inline static BOOL isNilOrEmpty(id value) +{ + if ([value isKindOfClass:NSString.class]) { + return [(NSString*)value length] == 0; + } + return value == nil; +} diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h new file mode 100644 index 0000000..cb2b539 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (FBUtf8SafeString) + +/** + Converts the string, so it could be properly represented in UTF-8 encoding. All non-encodable characters are replaced with + the given `replacement` + + @param replacement The character to use a a replacement for the lossy encoding + @returns Either the same string or a string with non-encodable chars replaced + */ +- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement; + +@end + +@interface NSDictionary (FBUtf8SafeDictionary) + +/** + Converts the dictionary, so it could be properly represented in UTF-8 encoding. All non-encodable characters + in string values are replaced with the Unocde question mark characters. Nested dictionaries and arrays are + processed recursively. + + @returns Either the same dictionary or a dictionary with non-encodable chars in string values replaced + */ +- (instancetype)fb_utf8SafeDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m new file mode 100644 index 0000000..0a5905d --- /dev/null +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSDictionary+FBUtf8SafeDictionary.h" + +const unichar REPLACER = 0xfffd; + +@implementation NSString (FBUtf8SafeString) + +- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement +{ + if ([self canBeConvertedToEncoding:NSUTF8StringEncoding]) { + return self; + } + + NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + NSString *convertedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSMutableString *result = [NSMutableString string]; + NSString *replacementStr = [NSString stringWithCharacters:&replacement length:1]; + NSUInteger originalIdx = 0; + NSUInteger convertedIdx = 0; + while (originalIdx < [self length] && convertedIdx < [convertedString length]) { + unichar originalChar = [self characterAtIndex:originalIdx]; + unichar convertedChar = [convertedString characterAtIndex:convertedIdx]; + + if (originalChar == convertedChar) { + [result appendString:[NSString stringWithCharacters:&originalChar length:1]]; + originalIdx++; + convertedIdx++; + continue; + } + + while (originalChar != convertedChar && originalIdx < [self length]) { + [result appendString:replacementStr]; + originalChar = [self characterAtIndex:++originalIdx]; + } + } + return result.copy; +} + +@end + +@implementation NSArray (FBUtf8SafeArray) + +- (instancetype)fb_utf8SafeArray +{ + NSMutableArray *result = [NSMutableArray array]; + for (id item in self) { + if ([item isKindOfClass:NSString.class]) { + [result addObject:[(NSString *)item fb_utf8SafeStringWithReplacement:REPLACER]]; + } else if ([item isKindOfClass:NSDictionary.class]) { + [result addObject:[(NSDictionary *)item fb_utf8SafeDictionary]]; + } else if ([item isKindOfClass:NSArray.class]) { + [result addObject:[(NSArray *)item fb_utf8SafeArray]]; + } else { + [result addObject:item]; + } + } + return result.copy; +} + +@end + +@implementation NSDictionary (FBUtf8SafeDictionary) + +- (instancetype)fb_utf8SafeDictionary +{ + NSMutableDictionary *result = [self mutableCopy]; + for (id key in self) { + id value = result[key]; + if ([value isKindOfClass:NSString.class]) { + result[key] = [(NSString *)value fb_utf8SafeStringWithReplacement:REPLACER]; + } else if ([value isKindOfClass:NSArray.class]) { + result[key] = [(NSArray *)value fb_utf8SafeArray]; + } else if ([value isKindOfClass:NSDictionary.class]) { + result[key] = [(NSDictionary *)value fb_utf8SafeDictionary]; + } + } + return result.copy; +} + +@end diff --git a/WebDriverAgentLib/Categories/NSExpression+FBFormat.h b/WebDriverAgentLib/Categories/NSExpression+FBFormat.h new file mode 100644 index 0000000..274d420 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSExpression+FBFormat.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSExpression (FBFormat) + +/** + Method used to normalize/verify NSExpression expressions before passing them to WDA. + Only expressions of NSKeyPathExpressionType are going to be verified. + Allowed property names are only these declared in FBElement protocol (property names are received in runtime) + and their shortcuts (without 'wd' prefix). All other property names are considered as unknown. + + @param input expression object received from user input + @return formatted expression + @throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol + */ ++ (instancetype)fb_wdExpressionWithExpression:(NSExpression *)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/NSExpression+FBFormat.m b/WebDriverAgentLib/Categories/NSExpression+FBFormat.m new file mode 100644 index 0000000..d4c32ae --- /dev/null +++ b/WebDriverAgentLib/Categories/NSExpression+FBFormat.m @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSExpression+FBFormat.h" + +#import "FBElementUtils.h" + +@implementation NSExpression (FBFormat) + ++ (instancetype)fb_wdExpressionWithExpression:(NSExpression *)input +{ + if ([input expressionType] != NSKeyPathExpressionType) { + return input; + } + + NSString *propName = [input keyPath]; + NSUInteger dotPos = [propName rangeOfString:@"."].location; + NSString *wdPropName; + if (NSNotFound == dotPos) { + wdPropName = [FBElementUtils wdAttributeNameForAttributeName:propName]; + } else { + NSString *actualPropName = [propName substringToIndex:dotPos]; + NSString *suffix = [propName substringFromIndex:(dotPos + 1)]; + wdPropName = [NSString stringWithFormat:@"%@.%@", [FBElementUtils wdAttributeNameForAttributeName:actualPropName], suffix]; + } + return [NSExpression expressionForKeyPath:wdPropName]; +} + +@end diff --git a/WebDriverAgentLib/Categories/NSString+FBVisualLength.h b/WebDriverAgentLib/Categories/NSString+FBVisualLength.h new file mode 100644 index 0000000..ec065e2 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSString+FBVisualLength.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface NSString (FBVisualLength) + +/** + Helper method that returns length of string with trimmed whitespaces + */ +- (NSUInteger)fb_visualLength; + +@end diff --git a/WebDriverAgentLib/Categories/NSString+FBVisualLength.m b/WebDriverAgentLib/Categories/NSString+FBVisualLength.m new file mode 100644 index 0000000..652f1a0 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSString+FBVisualLength.m @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSString+FBVisualLength.h" + +@implementation NSString (FBVisualLength) + +- (NSUInteger)fb_visualLength +{ + return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length; +} + +@end diff --git a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h new file mode 100644 index 0000000..7cdceb7 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (FBXMLSafeString) + +/** + Method used to normalize a string before passing it to XML document + + @param replacement The string to be used as a replacement for invalid XML characters + @return The string where all characters, which are not members of + XML Character Range definition (http://www.w3.org/TR/2008/REC-xml-20081126/#charsets), + are replaced + */ +- (NSString *)fb_xmlSafeStringWithReplacement:(NSString *)replacement; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m new file mode 100644 index 0000000..dbb7573 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSString+FBXMLSafeString.h" + +@implementation NSString (FBXMLSafeString) + +- (NSString *)fb_xmlSafeStringWithReplacement:(NSString *)replacement +{ + static NSMutableCharacterSet *invalidSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + invalidSet = [NSMutableCharacterSet characterSetWithRange:NSMakeRange(0x9, 1)]; + [invalidSet addCharactersInRange:NSMakeRange(0xA, 1)]; + [invalidSet addCharactersInRange:NSMakeRange(0xD, 1)]; + [invalidSet addCharactersInRange:NSMakeRange(0x20, 0xD7FF - 0x20 + 1)]; + [invalidSet addCharactersInRange:NSMakeRange(0xE000, 0xFFFD - 0xE000 + 1)]; + [invalidSet addCharactersInRange:NSMakeRange(0x10000, 0x10FFFF - 0x10000 + 1)]; + [invalidSet invert]; + }); + return [[self componentsSeparatedByCharactersInSet:invalidSet] componentsJoinedByString:replacement]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h new file mode 100644 index 0000000..d984e0b --- /dev/null +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "XCAXClient_iOS.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const FBSnapshotMaxDepthKey; + +void FBSetCustomParameterForElementSnapshot (NSString* name, id value); + +id __nullable FBGetCustomParameterForElementSnapshot (NSString *name); + +@interface XCAXClient_iOS (FBSnapshotReqParams) + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m new file mode 100644 index 0000000..0d39ba0 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCAXClient_iOS+FBSnapshotReqParams.h" + +#import + +/** + Available parameters with their default values for XCTest: + @"maxChildren" : (int)2147483647 + @"traverseFromParentsToChildren" : YES + @"maxArrayCount" : (int)2147483647 + @"snapshotKeyHonorModalViews" : NO + @"maxDepth" : (int)2147483647 + */ +NSString *const FBSnapshotMaxDepthKey = @"maxDepth"; + +static id (*original_defaultParameters)(id, SEL); +static id (*original_snapshotParameters)(id, SEL); +static NSDictionary *defaultRequestParameters; +static NSDictionary *defaultAdditionalRequestParameters; +static NSMutableDictionary *customRequestParameters; + +void FBSetCustomParameterForElementSnapshot (NSString *name, id value) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + customRequestParameters = [NSMutableDictionary new]; + }); + customRequestParameters[name] = value; +} + +id FBGetCustomParameterForElementSnapshot (NSString *name) +{ + return customRequestParameters[name]; +} + +static id swizzledDefaultParameters(id self, SEL _cmd) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultRequestParameters = original_defaultParameters(self, _cmd); + }); + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:defaultRequestParameters]; + [result addEntriesFromDictionary:defaultAdditionalRequestParameters ?: @{}]; + [result addEntriesFromDictionary:customRequestParameters ?: @{}]; + return result.copy; +} + +static id swizzledSnapshotParameters(id self, SEL _cmd) +{ + NSDictionary *result = original_snapshotParameters(self, _cmd); + defaultAdditionalRequestParameters = result; + return result; +} + + +@implementation XCAXClient_iOS (FBSnapshotReqParams) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + ++ (void)load +{ + Method original_defaultParametersMethod = class_getInstanceMethod(self.class, @selector(defaultParameters)); + IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters; + original_defaultParameters = (id (*)(id, SEL)) method_setImplementation(original_defaultParametersMethod, swizzledDefaultParametersImp); + + Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters")); + IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters; + original_snapshotParameters = (id (*)(id, SEL)) method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp); +} + +#pragma clang diagnostic pop + +@end diff --git a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h new file mode 100644 index 0000000..fc81daa --- /dev/null +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCTIssue (AMPatcher) + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m new file mode 100644 index 0000000..70472e4 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCTIssue+FBPatcher.h" + +#import + +static _Bool swizzledShouldInterruptTest(id self, SEL _cmd) +{ + return NO; +} + +@implementation XCTIssue (AMPatcher) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + ++ (void)load +{ + SEL originalShouldInterruptTest = NSSelectorFromString(@"shouldInterruptTest"); + if (nil == originalShouldInterruptTest) return; + Method originalShouldInterruptTestMethod = class_getInstanceMethod(self.class, originalShouldInterruptTest); + if (nil == originalShouldInterruptTestMethod) return; + method_setImplementation(originalShouldInterruptTestMethod, (IMP)swizzledShouldInterruptTest); +} + +#pragma clang diagnostic pop + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h new file mode 100644 index 0000000..01bea46 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface XCUIApplication (FBAlert) + +/* The accessiblity label used for Safari app */ +extern NSString *const FB_SAFARI_APP_NAME; + +/** + Retrieve the current alert element + + @return Alert element instance + */ +- (XCUIElement *)fb_alertElement; + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m new file mode 100644 index 0000000..36628bc --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplication+FBAlert.h" + +#import "FBMacros.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUtilities.h" + +#define MAX_CENTER_DELTA 10.0 + +NSString *const FB_SAFARI_APP_NAME = @"Safari"; + + +@implementation XCUIApplication (FBAlert) + +- (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *)scrollView + viewSnapshot:(id)viewSnapshot +{ + CGRect appFrame = viewSnapshot.frame; + NSPredicate *dstViewMatchPredicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) { + CGRect curFrame = snapshot.frame; + if (!CGRectEqualToRect(appFrame, curFrame) + && curFrame.origin.x > 0 && curFrame.size.width < appFrame.size.width) { + CGFloat possibleCenterX = (appFrame.size.width - curFrame.size.width) / 2; + return fabs(possibleCenterX - curFrame.origin.x) < MAX_CENTER_DELTA; + } + return NO; + }]; + NSPredicate *dstViewContainPredicate1 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeTextView]; + NSPredicate *dstViewContainPredicate2 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeButton]; + // Find the first XCUIElementTypeOther which is the grandchild of the web view + // and is horizontally aligned to the center of the screen + XCUIElement *candidate = [[[[[[scrollView descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:@"WebView"] + descendantsMatchingType:XCUIElementTypeOther] + matchingPredicate:dstViewMatchPredicate] + containingPredicate:dstViewContainPredicate1] + containingPredicate:dstViewContainPredicate2].allElementsBoundByIndex.firstObject; + + if (nil == candidate) { + return nil; + } + // ...and contains one to two buttons + // and conatins at least one text view + __block NSUInteger buttonsCount = 0; + __block NSUInteger textViewsCount = 0; + id snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_customSnapshot]; + [snapshot enumerateDescendantsUsingBlock:^(id descendant) { + XCUIElementType curType = descendant.elementType; + if (curType == XCUIElementTypeButton) { + buttonsCount++; + } else if (curType == XCUIElementTypeTextView) { + textViewsCount++; + } + }]; + return (buttonsCount >= 1 && buttonsCount <= 2 && textViewsCount > 0) ? candidate : nil; +} + +- (XCUIElement *)fb_alertElement +{ + NSPredicate *alertCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu,%lu}", + XCUIElementTypeAlert, XCUIElementTypeSheet, XCUIElementTypeScrollView]; + XCUIElement *alert = [[self descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:alertCollectorPredicate].allElementsBoundByIndex.firstObject; + if (nil == alert) { + return nil; + } + id alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_customSnapshot]; + + if (alertSnapshot.elementType == XCUIElementTypeAlert) { + return alert; + } + + if (alertSnapshot.elementType == XCUIElementTypeSheet) { + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { + return alert; + } + + // In case of iPad we want to check if sheet isn't contained by popover. + // In that case we ignore it. + id ancestor = alertSnapshot.parent; + while (nil != ancestor) { + if (nil != ancestor.identifier && [ancestor.identifier isEqualToString:@"PopoverDismissRegion"]) { + return nil; + } + ancestor = ancestor.parent; + } + return alert; + } + + if (alertSnapshot.elementType == XCUIElementTypeScrollView) { + id app = [[FBXCElementSnapshotWrapper ensureWrapped:alertSnapshot] fb_parentMatchingType:XCUIElementTypeApplication]; + if (nil != app && [app.label isEqualToString:FB_SAFARI_APP_NAME]) { + // Check alert presence in Safari web view + return [self fb_alertElementFromSafariWithScrollView:alert viewSnapshot:alertSnapshot]; + } + } + + return nil; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h b/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h new file mode 100644 index 0000000..4df5025 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "FBElement.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplication (FBFocused) + +/** + Return current focused element + */ +- (id)fb_focusedElement; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h new file mode 100644 index 0000000..f5d358f --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class XCElementSnapshot; +@protocol FBXCAccessibilityElement; +@class FBXMLGenerationOptions; + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplication (FBHelpers) + +/** + Deactivates application for given time + + @param duration amount of time application should deactivated + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error; + +/** + Return application elements tree in form of nested dictionaries + */ +- (NSDictionary *)fb_tree; + +/** + @param excludedAttributes Set of possible attributes to be excluded i.e frame, enabled, visible, accessible, focused. If set to nil or an empty array then no attributes will be excluded from the resulting JSON + @return application elements tree in form of nested dictionaries + */ +- (NSDictionary *)fb_tree:(nullable NSSet *) excludedAttributes; + +/** + Return application elements accessibility tree in form of nested dictionaries + */ +- (NSDictionary *)fb_accessibilityTree; + +/** + Return application elements tree in a form of xml string + with default options. + + @return nil if there was a failure while retriveing the page source. + */ +- (nullable NSString *)fb_xmlRepresentation; + +/** + Return application elements tree in a form of xml string + + @param options Optional values that affect the resulting XML generation process. + @return nil if there was a failure while retriveing the page source. + */ +- (nullable NSString *)fb_xmlRepresentationWithOptions:(nullable FBXMLGenerationOptions *)options; + +/** + Return application elements tree in form of internal XCTest debugDescription string + */ +- (NSString *)fb_descriptionRepresentation; + +/** + Returns the element, which currently holds the keyboard input focus or nil if there are no such elements. + */ +- (nullable XCUIElement *)fb_activeElement; + +#if TARGET_OS_TV +/** + Returns the element, which currently focused. + */ +- (nullable XCUIElement *)fb_focusedElement; +#endif + +/** + Waits until the current on-screen accessbility element belongs to the current application instance + @param timeout The maximum time to wait for the element to appear + @returns Either YES or NO + */ +- (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout; + +/** + Retrieves the information about the applications the given accessiblity elements + belong to + + @param axElements the list of accessibility elements + @returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items + */ ++ (NSArray *> *)fb_appsInfoWithAxElements:(NSArray> *)axElements; + +/** + Retrieves the information about the currently active apps + + @returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items. + */ ++ (NSArray *> *)fb_activeAppsInfo; + +/** + Tries to dismiss the on-screen keyboard + + @param keyNames Optional list of possible keyboard key labels to tap + in order to dismiss the keyboard. + @param error The resulting error object if the method fails to dismiss the keyboard + @returns YES if the keyboard dismissal was successful or NO otherwise + */ +- (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames + error:(NSError **)error; + +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; + +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; +/** + Constructor used to get current active application + */ ++ (instancetype)fb_activeApplication; + +/** + Constructor used to get current active application + + @param bundleId The bundle identifier of an app, which should be selected as active by default + if it is present in the list of active applications + */ ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId; + +/** + Constructor used to get the system application (e.g. Springboard on iOS) + */ ++ (instancetype)fb_systemApplication; + +/** + Retrieves the list of all currently active applications + */ ++ (NSArray *)fb_activeApplications; + +/** + Switch to system app (called Springboard on iOS) + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ ++ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error; + +/** + Determines whether the other app is the same as the current one + + @param otherApp Other app instance + @return YES if the other app has the same identifier + */ +- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m new file mode 100644 index 0000000..e5abde9 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -0,0 +1,644 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplication+FBHelpers.h" + +#import "FBActiveAppDetectionPoint.h" +#import "FBElementTypeTransformer.h" +#import "FBKeyboard.h" +#import "FBLogger.h" +#import "FBExceptions.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBRunLoopSpinner.h" +#import "FBXCodeCompatibility.h" +#import "FBXPath.h" +#import "FBXCAccessibilityElement.h" +#import "FBXCTestDaemonsProxy.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCAXClientProxy.h" +#import "FBXMLGenerationOptions.h" +#import "XCTestManager_ManagerInterface-Protocol.h" +#import "XCTestPrivateSymbols.h" +#import "XCTRunnerDaemonSession.h" +#import "XCUIApplication.h" +#import "XCUIApplicationImpl.h" +#import "XCUIApplicationProcess.h" +#import "XCUIDevice+FBHelpers.h" +#import "XCUIElement.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElementQuery.h" +#import "FBElementHelpers.h" + +static NSString* const FBUnknownBundleId = @"unknown"; + +static NSString* const FBExclusionAttributeFrame = @"frame"; +static NSString* const FBExclusionAttributeEnabled = @"enabled"; +static NSString* const FBExclusionAttributeVisible = @"visible"; +static NSString* const FBExclusionAttributeAccessible = @"accessible"; +static NSString* const FBExclusionAttributeFocused = @"focused"; +static NSString* const FBExclusionAttributePlaceholderValue = @"placeholderValue"; +static NSString* const FBExclusionAttributeNativeFrame = @"nativeFrame"; +static NSString* const FBExclusionAttributeTraits = @"traits"; +static NSString* const FBExclusionAttributeMinValue = @"minValue"; +static NSString* const FBExclusionAttributeMaxValue = @"maxValue"; + +_Nullable id extractIssueProperty(id issue, NSString *propertyName) { + SEL selector = NSSelectorFromString(propertyName); + NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; + if (nil == methodSignature) { + return nil; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:issue]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + return result; +} + +NSDictionary *auditTypeNamesToValues(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + // https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + result = @{ + @"XCUIAccessibilityAuditTypeAction": @(1UL << 32), + @"XCUIAccessibilityAuditTypeAll": @(~0UL), + @"XCUIAccessibilityAuditTypeContrast": @(1UL << 0), + @"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16), + @"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1), + @"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2), + @"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33), + @"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3), + @"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17), + @"XCUIAccessibilityAuditTypeTrait": @(1UL << 18), + }; + }); + return result; +} + +NSDictionary *auditTypeValuesToNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *inverted = [NSMutableDictionary new]; + [auditTypeNamesToValues() enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSNumber *value, BOOL *stop) { + inverted[value] = key; + }]; + result = inverted.copy; + }); + return result; +} + +NSDictionary *customExclusionAttributesMap(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + result = @{ + FBExclusionAttributeVisible: FB_XCAXAIsVisibleAttributeName, + FBExclusionAttributeAccessible: FB_XCAXAIsElementAttributeName, + }; + }); + return result; +} + +@implementation XCUIApplication (FBHelpers) + +- (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout +{ + __block BOOL canDetectAxElement = YES; + int currentProcessIdentifier = [self.accessibilityElement processIdentifier]; + BOOL result = [[[FBRunLoopSpinner new] + timeout:timeout] + spinUntilTrue:^BOOL{ + id currentAppElement = FBActiveAppDetectionPoint.sharedInstance.axElement; + canDetectAxElement = nil != currentAppElement; + if (!canDetectAxElement) { + return YES; + } + return currentAppElement.processIdentifier == currentProcessIdentifier; + }]; + return canDetectAxElement + ? result + : [self waitForExistenceWithTimeout:timeout]; +} + ++ (NSArray *> *)fb_appsInfoWithAxElements:(NSArray> *)axElements +{ + NSMutableArray *> *result = [NSMutableArray array]; + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; + for (id axElement in axElements) { + NSMutableDictionary *appInfo = [NSMutableDictionary dictionary]; + pid_t pid = axElement.processIdentifier; + appInfo[@"pid"] = @(pid); + __block NSString *bundleId = nil; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [proxy _XCT_requestBundleIDForPID:pid + reply:^(NSString *bundleID, NSError *error) { + if (nil == error) { + bundleId = bundleID; + } else { + [FBLogger logFmt:@"Cannot request the bundle ID for process ID %@: %@", @(pid), error.description]; + } + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC))); + appInfo[@"bundleId"] = bundleId ?: FBUnknownBundleId; + [result addObject:appInfo.copy]; + } + return result.copy; +} + ++ (NSArray *> *)fb_activeAppsInfo +{ + return [self fb_appsInfoWithAxElements:[FBXCAXClientProxy.sharedClient activeApplications]]; +} + +- (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error +{ + if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) { + return NO; + } + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, .0)]]; + [self activate]; + return YES; +} + +- (NSDictionary *)fb_tree +{ + return [self fb_tree:nil]; +} + +- (NSDictionary *)fb_tree:(nullable NSSet *)excludedAttributes +{ + id snapshot = [self fb_standardSnapshot]; + return [self.class dictionaryForElement:snapshot + recursive:YES + excludedAttributes:excludedAttributes]; +} + +- (NSDictionary *)fb_accessibilityTree +{ + id snapshot = [self fb_standardSnapshot]; + return [self.class accessibilityInfoForElement:snapshot]; +} + ++ (NSDictionary *)dictionaryForElement:(id)snapshot + recursive:(BOOL)recursive + excludedAttributes:(nullable NSSet *)excludedAttributes +{ + NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; + info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType]; + info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier); + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName); + info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue); + info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel); + info[@"rect"] = wrappedSnapshot.wdRect; + + NSDictionary *attributeBlocks = [self fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot]; + + NSSet *nonPrefixedKeys = [NSSet setWithObjects: + FBExclusionAttributeFrame, + FBExclusionAttributePlaceholderValue, + FBExclusionAttributeNativeFrame, + FBExclusionAttributeTraits, + FBExclusionAttributeMinValue, + FBExclusionAttributeMaxValue, + nil]; + + for (NSString *key in attributeBlocks) { + if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) { + NSString *value = ((NSString * (^)(void))attributeBlocks[key])(); + if ([nonPrefixedKeys containsObject:key]) { + info[key] = value; + } else { + info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value; + } + } + } + + if (!recursive) { + return info.copy; + } + + NSArray *childElements = snapshot.children; + if ([childElements count]) { + info[@"children"] = [[NSMutableArray alloc] init]; + for (id childSnapshot in childElements) { + @autoreleasepool { + [info[@"children"] addObject:[self dictionaryForElement:childSnapshot + recursive:YES + excludedAttributes:excludedAttributes]]; + } + } + } + return info; +} + +// Helper used by `dictionaryForElement:` to assemble attribute value blocks, +// including both common attributes and conditionally included ones like placeholderValue. ++ (NSDictionary *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot + +{ + // Base attributes common to every element + NSMutableDictionary *blocks = + [@{ + FBExclusionAttributeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdFrame); + }, + FBExclusionAttributeNativeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdNativeFrame); + }, + FBExclusionAttributeEnabled: ^{ + return [@([wrappedSnapshot isWDEnabled]) stringValue]; + }, + FBExclusionAttributeVisible: ^{ + return [@([wrappedSnapshot isWDVisible]) stringValue]; + }, + FBExclusionAttributeAccessible: ^{ + return [@([wrappedSnapshot isWDAccessible]) stringValue]; + }, + FBExclusionAttributeFocused: ^{ + return [@([wrappedSnapshot isWDFocused]) stringValue]; + }, + FBExclusionAttributeTraits: ^{ + return wrappedSnapshot.wdTraits; + } + } mutableCopy]; + + XCUIElementType elementType = wrappedSnapshot.elementType; + + // Text-input placeholder (only for elements that support inner text) + if (FBDoesElementSupportInnerText(elementType)) { + blocks[FBExclusionAttributePlaceholderValue] = ^{ + return (NSString *)FBValueOrNull(wrappedSnapshot.wdPlaceholderValue); + }; + } + + // Only for elements that support min/max value + if (FBDoesElementSupportMinMaxValue(elementType)) { + blocks[FBExclusionAttributeMinValue] = ^{ + return wrappedSnapshot.wdMinValue; + }; + blocks[FBExclusionAttributeMaxValue] = ^{ + return wrappedSnapshot.wdMaxValue; + }; + } + + return [blocks copy]; +} + ++ (NSDictionary *)accessibilityInfoForElement:(id)snapshot +{ + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + BOOL isAccessible = [wrappedSnapshot isWDAccessible]; + BOOL isVisible = [wrappedSnapshot isWDVisible]; + + NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; + + if (isAccessible) { + if (isVisible) { + info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue); + info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel); + } + } else { + NSMutableArray *children = [[NSMutableArray alloc] init]; + for (id childSnapshot in snapshot.children) { + @autoreleasepool { + NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot]; + if ([childInfo count]) { + [children addObject: childInfo]; + } + } + } + if ([children count]) { + info[@"children"] = [children copy]; + } + } + if ([info count]) { + info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType]; + info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier); + info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName); + } else { + return nil; + } + return info; +} + +- (NSString *)fb_xmlRepresentation +{ + return [self fb_xmlRepresentationWithOptions:nil]; +} + +- (NSString *)fb_xmlRepresentationWithOptions:(FBXMLGenerationOptions *)options +{ + return [FBXPath xmlStringWithRootElement:self options:options]; +} + +- (NSString *)fb_descriptionRepresentation +{ + NSMutableArray *childrenDescriptions = [NSMutableArray array]; + for (XCUIElement *child in [self.fb_query childrenMatchingType:XCUIElementTypeAny].allElementsBoundByIndex) { + [childrenDescriptions addObject:child.debugDescription]; + } + // debugDescription property of XCUIApplication instance shows descendants addresses in memory + // instead of the actual information about them, however the representation works properly + // for all descendant elements + return (0 == childrenDescriptions.count) ? self.debugDescription : [childrenDescriptions componentsJoinedByString:@"\n\n"]; +} + +- (XCUIElement *)fb_activeElement +{ + return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:[NSPredicate predicateWithFormat:@"hasKeyboardFocus == YES"]] + fb_firstMatch]; +} + +#if TARGET_OS_TV +- (XCUIElement *)fb_focusedElement +{ + return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:[NSPredicate predicateWithFormat:@"hasFocus == true"]] + fb_firstMatch]; +} +#endif + +- (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames + error:(NSError **)error +{ + BOOL (^isKeyboardInvisible)(void) = ^BOOL(void) { + return ![FBKeyboard waitUntilVisibleForApplication:self + timeout:0 + error:nil]; + }; + + if (isKeyboardInvisible()) { + // Short circuit if the keyboard is not visible + return YES; + } + +#if TARGET_OS_TV + [[XCUIRemote sharedRemote] pressButton:XCUIRemoteButtonMenu]; +#else + NSArray *(^findMatchingKeys)(NSPredicate *) = ^NSArray *(NSPredicate * predicate) { + NSPredicate *keysPredicate = [NSPredicate predicateWithFormat:@"elementType == %@", @(XCUIElementTypeKey)]; + XCUIElementQuery *parentView = [[self.keyboard descendantsMatchingType:XCUIElementTypeOther] + containingPredicate:keysPredicate]; + return [[parentView childrenMatchingType:XCUIElementTypeAny] + matchingPredicate:predicate].allElementsBoundByIndex; + }; + + if (nil != keyNames && keyNames.count > 0) { + NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) { + if (snapshot.elementType != XCUIElementTypeKey && snapshot.elementType != XCUIElementTypeButton) { + return NO; + } + + return (nil != snapshot.identifier && [keyNames containsObject:snapshot.identifier]) + || (nil != snapshot.label && [keyNames containsObject:snapshot.label]); + }]; + NSArray *matchedKeys = findMatchingKeys(searchPredicate); + if (matchedKeys.count > 0) { + for (XCUIElement *matchedKey in matchedKeys) { + if (!matchedKey.exists) { + continue; + } + + [matchedKey tap]; + if (isKeyboardInvisible()) { + return YES; + } + } + } + } + + if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"elementType IN %@", + @[@(XCUIElementTypeKey), @(XCUIElementTypeButton)]]; + NSArray *matchedKeys = findMatchingKeys(searchPredicate); + if (matchedKeys.count > 0) { + [matchedKeys[matchedKeys.count - 1] tap]; + } + } +#endif + NSString *errorDescription = @"Did not know how to dismiss the keyboard. Try to dismiss it in the way supported by your application under test."; + return [[[[FBRunLoopSpinner new] + timeout:3] + timeoutErrorMessage:errorDescription] + spinUntilTrue:isKeyboardInvisible + error:error]; +} + +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; +{ + uint64_t numTypes = 0; + NSDictionary *namesMap = auditTypeNamesToValues(); + for (NSString *value in auditTypes) { + NSNumber *typeValue = namesMap[value]; + if (nil == typeValue) { + NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys]; + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; + } + numTypes |= [typeValue unsignedLongLongValue]; + } + return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error]; +} + +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; +{ + SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:"); + if (![self respondsToSelector:selector]) { + [[[FBErrorBuilder alloc] + withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"] + buildError:error]; + return nil; + } + + // These custom attributes could take too long to fetch, thus excluded + NSSet *customAttributesToExclude = [NSSet setWithArray:[customExclusionAttributesMap() allKeys]]; + NSMutableArray *resultArray = [NSMutableArray array]; + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setArgument:&auditTypes atIndex:2]; + BOOL (^issueHandler)(id) = ^BOOL(id issue) { + @autoreleasepool { + NSString *auditType = @""; + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; + if (nil != auditTypeValue) { + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + } + + id extractedElement = extractIssueProperty(issue, @"element"); + + id elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_standardSnapshot]; + NSDictionary *elementAttributes = elementSnapshot + ? [self.class dictionaryForElement:elementSnapshot + recursive:NO + excludedAttributes:customAttributesToExclude] + : @{}; + + [resultArray addObject:@{ + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", + @"auditType": auditType, + @"element": [extractedElement description] ?: @"", + @"elementDescription": [extractedElement debugDescription] ?: @"", + @"elementAttributes": elementAttributes ?: @{}, + }]; + return YES; + } + }; + [invocation setArgument:&issueHandler atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invokeWithTarget:self]; + BOOL isSuccessful; + [invocation getReturnValue:&isSuccessful]; + return isSuccessful ? resultArray.copy : nil; +} + ++ (instancetype)fb_activeApplication +{ + return [self fb_activeApplicationWithDefaultBundleId:nil]; +} + ++ (NSArray *)fb_activeApplications +{ + NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; + NSMutableArray *result = [NSMutableArray array]; + if (activeApplicationElements.count > 0) { + for (id applicationElement in activeApplicationElements) { + XCUIApplication *app = [XCUIApplication fb_applicationWithPID:applicationElement.processIdentifier]; + if (nil != app) { + [result addObject:app]; + } + } + } + return result.count > 0 ? result.copy : @[self.class.fb_systemApplication]; +} + ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId +{ + NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; + id activeApplicationElement = nil; + id currentElement = nil; + if (nil != bundleId) { + currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; + if (nil != currentElement) { + NSArray *appInfos = [self fb_appsInfoWithAxElements:@[currentElement]]; + [FBLogger logFmt:@"Detected on-screen application: %@", appInfos.firstObject[@"bundleId"]]; + if ([[appInfos.firstObject objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { + activeApplicationElement = currentElement; + } + } + } + if (nil == activeApplicationElement && activeApplicationElements.count > 1) { + if (nil != bundleId) { + NSArray *appInfos = [self fb_appsInfoWithAxElements:activeApplicationElements]; + NSMutableArray *bundleIds = [NSMutableArray array]; + for (NSDictionary *appInfo in appInfos) { + [bundleIds addObject:(NSString *)appInfo[@"bundleId"]]; + } + [FBLogger logFmt:@"Detected system active application(s): %@", bundleIds]; + // Try to select the desired application first + for (NSUInteger appIdx = 0; appIdx < appInfos.count; appIdx++) { + if ([[[appInfos objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { + activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx]; + break; + } + } + } + // Fall back to the "normal" algorithm if the desired application is either + // not set or is not active + if (nil == activeApplicationElement) { + if (nil == currentElement) { + currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; + } + if (nil == currentElement) { + [FBLogger log:@"Cannot precisely detect the current application. Will use the system's recently active one"]; + if (nil == bundleId) { + [FBLogger log:@"Consider changing the 'defaultActiveApplication' setting to the bundle identifier of the desired application under test"]; + } + } else { + for (id appElement in activeApplicationElements) { + if (appElement.processIdentifier == currentElement.processIdentifier) { + activeApplicationElement = appElement; + break; + } + } + } + } + } + + if (nil != activeApplicationElement) { + XCUIApplication *application = [XCUIApplication fb_applicationWithPID:activeApplicationElement.processIdentifier]; + if (nil != application) { + return application; + } + [FBLogger log:@"Cannot translate the active process identifier into an application object"]; + } + + if (activeApplicationElements.count > 0) { + [FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)]; + for (id appElement in activeApplicationElements) { + XCUIApplication *application = [XCUIApplication fb_applicationWithPID:appElement.processIdentifier]; + if (nil != application) { + return application; + } + } + } + + [FBLogger log:@"Cannot retrieve any active applications. Assuming the system application is the active one"]; + return [self fb_systemApplication]; +} + ++ (instancetype)fb_systemApplication +{ + return [self fb_applicationWithPID: + [[FBXCAXClientProxy.sharedClient systemApplication] processIdentifier]]; +} + ++ (instancetype)fb_applicationWithPID:(pid_t)processID +{ + return [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:processID]; +} + ++ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error +{ + XCUIApplication *systemApp = self.fb_systemApplication; + @try { + if (systemApp.running) { + [systemApp activate]; + } else { + [systemApp launch]; + } + } @catch (NSException *e) { + return [[[FBErrorBuilder alloc] + withDescription:nil == e ? @"Cannot open the home screen" : e.reason] + buildError:error]; + } + return YES; +} + +- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp +{ + if (nil == otherApp) { + return NO; + } + return self == otherApp || [self.bundleID isEqualToString:(NSString *)otherApp.bundleID]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h new file mode 100644 index 0000000..962a481 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "XCUIApplication.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplication (FBQuiescence) + +/** + It allows to turn on/off waiting for application quiescence, while performing queries. Defaults to YES. + This value mirrors the corresponding property of the connected XCUIApplicationProcess instance. + */ +@property (nonatomic, assign) BOOL fb_shouldWaitForQuiescence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m new file mode 100644 index 0000000..72febaa --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplication+FBQuiescence.h" + +#import "XCUIApplicationImpl.h" +#import "XCUIApplicationProcess.h" +#import "XCUIApplicationProcess+FBQuiescence.h" + + +@implementation XCUIApplication (FBQuiescence) + +- (BOOL)fb_shouldWaitForQuiescence +{ + return [[self applicationImpl] currentProcess].fb_shouldWaitForQuiescence.boolValue; +} + +- (void)setFb_shouldWaitForQuiescence:(BOOL)value +{ + [[self applicationImpl] currentProcess].fb_shouldWaitForQuiescence = @(value); +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h new file mode 100644 index 0000000..79b220c --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +#import +#import "FBElementCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplication (FBTouchAction) + +/** + Perform complex touch action in scope of the current application. + + @param actions Array of dictionaries, whose format is described in W3C spec (https://github.com/jlipps/simple-wd-spec#perform-actions) + @param elementCache Cached elements mapping for the currrent application. The method assumes all elements are already represented by their actual instances if nil value is set + @param error If there is an error, upon return contains an NSError object that describes the problem + @return YES If the touch action has been successfully performed without errors + */ +- (BOOL)fb_performW3CActions:(NSArray *)actions elementCache:(nullable FBElementCache *)elementCache error:(NSError * _Nullable*)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m new file mode 100644 index 0000000..bbd1e57 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +#import "XCUIApplication+FBTouchAction.h" + +#import "FBBaseActionsSynthesizer.h" +#import "FBConfiguration.h" +#import "FBExceptions.h" +#import "FBLogger.h" +#import "FBRunLoopSpinner.h" +#import "FBW3CActionsSynthesizer.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCEventGenerator.h" +#import "XCUIElement+FBUtilities.h" + +#if !TARGET_OS_TV + +@implementation XCUIApplication (FBTouchAction) + ++ (BOOL)handleEventSynthesWithError:(NSError *)error +{ + if ([error.localizedDescription containsString:@"not visible"]) { + [[NSException exceptionWithName:FBElementNotVisibleException + reason:error.localizedDescription + userInfo:error.userInfo] raise]; + } + return NO; +} + +- (BOOL)fb_performActionsWithSynthesizerType:(Class)synthesizerType + actions:(NSArray *)actions + elementCache:(FBElementCache *)elementCache + error:(NSError **)error +{ + FBBaseActionsSynthesizer *synthesizer = [[synthesizerType alloc] initWithActions:actions + forApplication:self + elementCache:elementCache + error:error]; + if (nil == synthesizer) { + return NO; + } + XCSynthesizedEventRecord *eventRecord = [synthesizer synthesizeWithError:error]; + if (nil == eventRecord) { + return [self.class handleEventSynthesWithError:*error]; + } + return [self fb_synthesizeEvent:eventRecord error:error]; +} + +- (BOOL)fb_performW3CActions:(NSArray *)actions + elementCache:(FBElementCache *)elementCache + error:(NSError **)error +{ + if (![self fb_performActionsWithSynthesizerType:FBW3CActionsSynthesizer.class + actions:actions + elementCache:elementCache + error:error]) { + return NO; + } + [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + return YES; +} + +- (BOOL)fb_synthesizeEvent:(XCSynthesizedEventRecord *)event error:(NSError *__autoreleasing*)error +{ + return [FBXCTestDaemonsProxy synthesizeEventWithRecord:event error:error]; +} + +@end +#endif diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h new file mode 100644 index 0000000..e952732 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplication (FBUIInterruptions) + +/** + * Disables automatic UI interruptions handling for all applications. + */ ++ (void)fb_disableUIInterruptionsHandling; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m new file mode 100644 index 0000000..b662a1e --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplication+FBUIInterruptions.h" + +#import "FBReflectionUtils.h" +#import "XCUIApplication.h" + +@implementation XCUIApplication (FBUIInterruptions) + +- (BOOL)fb_doesNotHandleUIInterruptions +{ + return YES; +} + ++ (void)fb_disableUIInterruptionsHandling +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + FBReplaceMethod([self class], + @selector(doesNotHandleUIInterruptions), + @selector(fb_doesNotHandleUIInterruptions)); + }); +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h new file mode 100644 index 0000000..f911cbf --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "XCUIApplicationProcess.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIApplicationProcess (FBQuiescence) + +/*! Defines wtether the process should perform quiescence checks. YES by default */ +@property (nonatomic) NSNumber* fb_shouldWaitForQuiescence; + +/** + @param waitForAnimations Set it to YES if XCTest should also wait for application animations to complete + */ +- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m new file mode 100644 index 0000000..1f6272d --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplicationProcess+FBQuiescence.h" + +#import + +#import "FBConfiguration.h" +#import "FBExceptions.h" +#import "FBLogger.h" +#import "FBSettings.h" + +static void (*original_waitForQuiescenceIncludingAnimationsIdle)(id, SEL, BOOL); +static void (*original_waitForQuiescenceIncludingAnimationsIdlePreEvent)(id, SEL, BOOL, BOOL); + +static void swizzledWaitForQuiescenceIncludingAnimationsIdle(id self, SEL _cmd, BOOL includingAnimations) +{ + NSString *bundleId = [self bundleID]; + if (![[self fb_shouldWaitForQuiescence] boolValue] || FBConfiguration.waitForIdleTimeout < DBL_EPSILON) { + [FBLogger logFmt:@"Quiescence checks are disabled for %@ application. Making it to believe it is idling", + bundleId]; + return; + } + + NSTimeInterval desiredTimeout = FBConfiguration.waitForIdleTimeout; + NSTimeInterval previousTimeout = _XCTApplicationStateTimeout(); + _XCTSetApplicationStateTimeout(desiredTimeout); + [FBLogger logFmt:@"Waiting up to %@s until %@ is in idle state (%@ animations)", + @(desiredTimeout), bundleId, includingAnimations ? @"including" : @"excluding"]; + @try { + original_waitForQuiescenceIncludingAnimationsIdle(self, _cmd, includingAnimations); + } @finally { + _XCTSetApplicationStateTimeout(previousTimeout); + } +} + +static void swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent(id self, SEL _cmd, BOOL includingAnimations, BOOL isPreEvent) +{ + NSString *bundleId = [self bundleID]; + if (![[self fb_shouldWaitForQuiescence] boolValue] || FBConfiguration.waitForIdleTimeout < DBL_EPSILON) { + [FBLogger logFmt:@"Quiescence checks are disabled for %@ application. Making it to believe it is idling", + bundleId]; + return; + } + + NSTimeInterval desiredTimeout = FBConfiguration.waitForIdleTimeout; + NSTimeInterval previousTimeout = _XCTApplicationStateTimeout(); + _XCTSetApplicationStateTimeout(desiredTimeout); + [FBLogger logFmt:@"Waiting up to %@s until %@ is in idle state (%@ animations)", + @(desiredTimeout), bundleId, includingAnimations ? @"including" : @"excluding"]; + @try { + original_waitForQuiescenceIncludingAnimationsIdlePreEvent(self, _cmd, includingAnimations, isPreEvent); + } @finally { + _XCTSetApplicationStateTimeout(previousTimeout); + } +} + +@implementation XCUIApplicationProcess (FBQuiescence) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + ++ (void)load +{ + Method waitForQuiescenceIncludingAnimationsIdleMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:)); + Method waitForQuiescenceIncludingAnimationsIdlePreEventMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:)); + if (nil != waitForQuiescenceIncludingAnimationsIdleMethod) { + IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdle; + original_waitForQuiescenceIncludingAnimationsIdle = (void (*)(id, SEL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdleMethod, swizzledImp); + } else if (nil != waitForQuiescenceIncludingAnimationsIdlePreEventMethod) { + IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent; + original_waitForQuiescenceIncludingAnimationsIdlePreEvent = (void (*)(id, SEL, BOOL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdlePreEventMethod, swizzledImp); + } else { + [FBLogger log:@"Could not find method -[XCUIApplicationProcess waitForQuiescenceIncludingAnimationsIdle:]"]; + } +} + +#pragma clang diagnostic pop + +static char XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE; + +@dynamic fb_shouldWaitForQuiescence; + +- (NSNumber *)fb_shouldWaitForQuiescence +{ + id result = objc_getAssociatedObject(self, &XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE); + if (nil == result) { + return @(YES); + } + return (NSNumber *)result; +} + +- (void)setFb_shouldWaitForQuiescence:(NSNumber *)value +{ + objc_setAssociatedObject(self, &XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations +{ + if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:)]) { + [self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations]; + } else if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:)]) { + [self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations isPreEvent:NO]; + } else { + @throw [NSException exceptionWithName:FBIncompatibleWdaException + reason:@"The current WebDriverAgent build is not compatible to your device OS version" + userInfo:@{}]; + } +} + + +@end diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h new file mode 100644 index 0000000..1c1ab63 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + + +@interface XCUIDevice (FBHealthCheck) + +/** + Checks health of XCTest by: + 1) Querying application for some elements, + 2) Triggering some device events. + + !!! Health check might modify simulator state so it should only be called in-between testing sessions + + @param application application used to issue queries + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_healthCheckWithApplication:(nullable XCUIApplication *)application; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m new file mode 100644 index 0000000..8f9de6e --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIDevice+FBHealthCheck.h" + +#import "XCUIDevice+FBRotation.h" +#import "XCUIApplication+FBHelpers.h" + +@implementation XCUIDevice (FBHealthCheck) + +- (BOOL)fb_healthCheckWithApplication:(nullable XCUIApplication *)application +{ + if (![self fb_elementQueryCheckWithApplication:application]) { + return NO; + } + if (![self fb_deviceInteractionCheck]) { + return NO; + } + return YES; +} + +- (BOOL)fb_elementQueryCheckWithApplication:(nullable XCUIApplication *)application +{ + if (!application) { + return NO; + } + if (!application.label) { + return NO; + } + if ([application descendantsMatchingType:XCUIElementTypeAny].count == 0 ) { + return NO; + } + return YES; +} + +- (BOOL)fb_deviceInteractionCheck +{ + [self pressButton:XCUIDeviceButtonHome]; + return YES; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h new file mode 100644 index 0000000..2f9c9d8 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#if !TARGET_OS_TV +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { + FBUIInterfaceAppearanceUnspecified, + FBUIInterfaceAppearanceLight, + FBUIInterfaceAppearanceDark +}; + +@interface XCUIDevice (FBHelpers) + +/** + Matches or mismatches TouchID request + + @param shouldMatch determines if TouchID should be matched + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_fingerTouchShouldMatch:(BOOL)shouldMatch; + +/** + Forces the device under test to switch to the home screen + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_goToHomescreenWithError:(NSError **)error; + +/** + Checks if the screen is locked or not. + + @return YES if screen is locked + */ +- (BOOL)fb_isScreenLocked; + +/** + Forces the device under test to switch to the lock screen. An immediate return will happen if the device is already locked and an error is going to be thrown if the screen has not been locked after the timeout. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_lockScreen:(NSError **)error; + +/** + Forces the device under test to unlock. An immediate return will happen if the device is already unlocked and an error is going to be thrown if the screen has not been unlocked after the timeout. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_unlockScreen:(NSError **)error; + +/** + Returns screenshot + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return Device screenshot as PNG-encoded data or nil in case of failure + */ +- (nullable NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error; + +/** + Returns device's current wifi ip4 address + */ +- (nullable NSString *)fb_wifiIPAddress; + +/** + Opens the particular url scheme using the default application assigned to it. + This API only works since XCode 14.3/iOS 16.4 + Older Xcode/iOS version try to use Siri fallback. + + @param url The url scheme represented as a string, for example https://apple.com + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation was successful + */ +- (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error; + +/** + Opens the particular url scheme using the given application + This API only works since XCode 14.3/iOS 16.4 + + @param url The url scheme represented as a string, for example https://apple.com + @param bundleId The bundle identifier of an application to use in order to open the given URL + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation was successful + */ +- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error; + +/** + Presses the corresponding hardware button on the device with duration. + + @param buttonName One of the supported button names: volumeUp (real devices only), volumeDown (real device only), home + @param duration Duration in seconds or nil. + This argument works only on tvOS. When this argument is nil on tvOS, + https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton will be called. + Others are https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton. + A single tap when this argument is `nil` is equal to when the duration is 0.005 seconds in XCTest. + On iOS, this value will be ignored. It always calls https://developer.apple.com/documentation/xctest/xcuidevice/1619052-pressbutton + @return YES if the button has been pressed + */ +- (BOOL)fb_pressButton:(NSString *)buttonName forDuration:(nullable NSNumber *)duration error:(NSError **)error; + + +/** + Activates Siri service voice recognition with the given text to parse + + @param text The actual string to parse + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES the command has been successfully executed by Siri voice recognition service + */ +- (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error; + +/** + Emulated triggering of the given low-level IOHID device event. The constants for possible events are defined + in https://unix.superglobalmegacorp.com/xnu/newsrc/iokit/IOKit/hidsystem/IOHIDUsageTables.h.html + Popular constants: + - kHIDPage_Consumer = 0x0C + - kHIDUsage_Csmr_VolumeIncrement = 0xE9 (Volume Up) + - kHIDUsage_Csmr_VolumeDecrement = 0xEA (Volume Down) + - kHIDUsage_Csmr_Menu = 0x40 (Home) + - kHIDUsage_Csmr_Power = 0x30 (Power) + - kHIDUsage_Csmr_Snapshot = 0x65 (Power + Home) + + @param page The event page identifier + @param usage The event usage identifier (usages are defined per-page) + @param duration The event duration in float seconds (XCTest uses 0.005 for a single press event) + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES the event has successfully been triggered + */ +- (BOOL)fb_performIOHIDEventWithPage:(unsigned int)page + usage:(unsigned int)usage + duration:(NSTimeInterval)duration + error:(NSError **)error; + +/** + Allows to set device appearance + + @param appearance The desired appearance value + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the appearance has been successfully set + */ +- (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)error; + +/** + Get current appearance prefefence. + + @return 0 (automatic), 1 (light) or 2 (dark), or nil + */ +- (nullable NSNumber *)fb_getAppearance; + +#if !TARGET_OS_TV +/** + Allows to set a simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param location The simlated location coordinates to set + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the simulated location has been successfully set + */ +- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error; + +/** + Allows to get a simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return The current simulated location or nil in case of failure or if no location has previously been seet + (the returned error will be nil in the latter case) + */ +- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error; + +/** + Allows to clear a previosuly set simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the simulated location has been successfully cleared + */ +- (BOOL)fb_clearSimulatedLocation:(NSError **)error; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m new file mode 100644 index 0000000..6608856 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -0,0 +1,388 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIDevice+FBHelpers.h" + +#import +#import +#include +#import + +#import "FBErrorBuilder.h" +#import "FBImageUtils.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBScreenshot.h" +#import "FBXCDeviceEvent.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCUIDevice.h" + +static const NSTimeInterval FBHomeButtonCoolOffTime = 1.; +static const NSTimeInterval FBScreenLockTimeout = 5.; + +@implementation XCUIDevice (FBHelpers) + +static bool fb_isLocked; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + ++ (void)load +{ + [self fb_registerAppforDetectLockState]; +} + +#pragma clang diagnostic pop + ++ (void)fb_registerAppforDetectLockState +{ + int notify_token; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wstrict-prototypes" + notify_register_dispatch("com.apple.springboard.lockstate", ¬ify_token, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) { + uint64_t state = UINT64_MAX; + notify_get_state(token, &state); + fb_isLocked = state != 0; + }); +#pragma clang diagnostic pop +} + +- (BOOL)fb_goToHomescreenWithError:(NSError **)error +{ + return [XCUIApplication fb_switchToSystemApplicationWithError:error]; +} + +- (BOOL)fb_lockScreen:(NSError **)error +{ + if (fb_isLocked) { + return YES; + } + [self pressLockButton]; + return [[[[FBRunLoopSpinner new] + timeout:FBScreenLockTimeout] + timeoutErrorMessage:@"Timed out while waiting until the screen gets locked"] + spinUntilTrue:^BOOL{ + return fb_isLocked; + } error:error]; +} + +- (BOOL)fb_isScreenLocked +{ + return fb_isLocked; +} + +- (BOOL)fb_unlockScreen:(NSError **)error +{ + if (!fb_isLocked) { + return YES; + } + [self pressButton:XCUIDeviceButtonHome]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]]; +#if !TARGET_OS_TV + [self pressButton:XCUIDeviceButtonHome]; +#else + [self pressButton:XCUIDeviceButtonHome]; +#endif + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]]; + return [[[[FBRunLoopSpinner new] + timeout:FBScreenLockTimeout] + timeoutErrorMessage:@"Timed out while waiting until the screen gets unlocked"] + spinUntilTrue:^BOOL{ + return !fb_isLocked; + } error:error]; +} + +- (NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error +{ + return [FBScreenshot takeInOriginalResolutionWithQuality:FBConfiguration.screenshotQuality + error:error]; +} + +- (BOOL)fb_fingerTouchShouldMatch:(BOOL)shouldMatch +{ + const char *name; + if (shouldMatch) { + name = "com.apple.BiometricKit_Sim.fingerTouch.match"; + } else { + name = "com.apple.BiometricKit_Sim.fingerTouch.nomatch"; + } + return notify_post(name) == NOTIFY_STATUS_OK; +} + +- (NSString *)fb_wifiIPAddress +{ + struct ifaddrs *interfaces = NULL; + struct ifaddrs *temp_addr = NULL; + int success = getifaddrs(&interfaces); + if (success != 0) { + freeifaddrs(interfaces); + return nil; + } + + NSString *address = nil; + temp_addr = interfaces; + while(temp_addr != NULL) { + if(temp_addr->ifa_addr->sa_family != AF_INET) { + temp_addr = temp_addr->ifa_next; + continue; + } + NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name]; + if(![interfaceName isEqualToString:@"en0"]) { + temp_addr = temp_addr->ifa_next; + continue; + } + address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)]; + break; + } + freeifaddrs(interfaces); + return address; +} + +- (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error +{ + NSURL *parsedUrl = [NSURL URLWithString:url]; + if (nil == parsedUrl) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"'%@' is not a valid URL", url] + buildError:error]; + } + + NSError *err; + if ([FBXCTestDaemonsProxy openDefaultApplicationForURL:parsedUrl error:&err]) { + return YES; + } + if (![err.description containsString:@"does not support"]) { + if (error) { + *error = err; + } + return NO; + } + + id siriService = [self valueForKey:@"siriService"]; + if (nil != siriService) { + return [self fb_activateSiriVoiceRecognitionWithText:[NSString stringWithFormat:@"Open {%@}", url] error:error]; + } + + NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url]; + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"%@", description] + buildError:error];; +} + +- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error +{ + NSURL *parsedUrl = [NSURL URLWithString:url]; + if (nil == parsedUrl) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"'%@' is not a valid URL", url] + buildError:error]; + } + + return [FBXCTestDaemonsProxy openURL:parsedUrl usingApplication:bundleId error:error]; +} + +- (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error +{ + id siriService = [self valueForKey:@"siriService"]; + if (nil == siriService) { + return [[[FBErrorBuilder builder] + withDescription:@"Siri service is not available on the device under test"] + buildError:error]; + } + SEL selector = NSSelectorFromString(@"activateWithVoiceRecognitionText:"); + NSMethodSignature *signature = [siriService methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setArgument:&text atIndex:2]; + @try { + [invocation invokeWithTarget:siriService]; + return YES; + } @catch (NSException *e) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"%@", e.reason] + buildError:error]; + } +} + +- (BOOL)fb_pressButton:(NSString *)buttonName + forDuration:(nullable NSNumber *)duration + error:(NSError **)error +{ +#if !TARGET_OS_TV + return [self fb_pressButton:buttonName error:error]; +#else + NSMutableArray *supportedButtonNames = [NSMutableArray array]; + NSInteger remoteButton = -1; // no remote button + if ([buttonName.lowercaseString isEqualToString:@"home"]) { + // XCUIRemoteButtonHome = 7 + remoteButton = XCUIRemoteButtonHome; + } + [supportedButtonNames addObject:@"home"]; + + // https://developer.apple.com/design/human-interface-guidelines/tvos/remote-and-controllers/remote/ + if ([buttonName.lowercaseString isEqualToString:@"up"]) { + // XCUIRemoteButtonUp = 0, + remoteButton = XCUIRemoteButtonUp; + } + [supportedButtonNames addObject:@"up"]; + + if ([buttonName.lowercaseString isEqualToString:@"down"]) { + // XCUIRemoteButtonDown = 1, + remoteButton = XCUIRemoteButtonDown; + } + [supportedButtonNames addObject:@"down"]; + + if ([buttonName.lowercaseString isEqualToString:@"left"]) { + // XCUIRemoteButtonLeft = 2, + remoteButton = XCUIRemoteButtonLeft; + } + [supportedButtonNames addObject:@"left"]; + + if ([buttonName.lowercaseString isEqualToString:@"right"]) { + // XCUIRemoteButtonRight = 3, + remoteButton = XCUIRemoteButtonRight; + } + [supportedButtonNames addObject:@"right"]; + + if ([buttonName.lowercaseString isEqualToString:@"menu"]) { + // XCUIRemoteButtonMenu = 5, + remoteButton = XCUIRemoteButtonMenu; + } + [supportedButtonNames addObject:@"menu"]; + + if ([buttonName.lowercaseString isEqualToString:@"playpause"]) { + // XCUIRemoteButtonPlayPause = 6, + remoteButton = XCUIRemoteButtonPlayPause; + } + [supportedButtonNames addObject:@"playpause"]; + + if ([buttonName.lowercaseString isEqualToString:@"select"]) { + // XCUIRemoteButtonSelect = 4, + remoteButton = XCUIRemoteButtonSelect; + } + [supportedButtonNames addObject:@"select"]; + + if (remoteButton == -1) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames] + buildError:error]; + } + + if (duration) { + // https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton + [[XCUIRemote sharedRemote] pressButton:remoteButton forDuration:duration.doubleValue]; + } else { + // https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton + [[XCUIRemote sharedRemote] pressButton:remoteButton]; + } + + return YES; +#endif +} + +#if !TARGET_OS_TV +- (BOOL)fb_pressButton:(NSString *)buttonName + error:(NSError **)error +{ + NSMutableArray *supportedButtonNames = [NSMutableArray array]; + XCUIDeviceButton dstButton = 0; + if ([buttonName.lowercaseString isEqualToString:@"home"]) { + dstButton = XCUIDeviceButtonHome; + } + [supportedButtonNames addObject:@"home"]; +#if !TARGET_OS_SIMULATOR + if ([buttonName.lowercaseString isEqualToString:@"volumeup"]) { + dstButton = XCUIDeviceButtonVolumeUp; + } + if ([buttonName.lowercaseString isEqualToString:@"volumedown"]) { + dstButton = XCUIDeviceButtonVolumeDown; + } + [supportedButtonNames addObject:@"volumeUp"]; + [supportedButtonNames addObject:@"volumeDown"]; +#endif + + if (dstButton == 0) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames] + buildError:error]; + } + [self pressButton:dstButton]; + return YES; +} +#endif + +- (BOOL)fb_performIOHIDEventWithPage:(unsigned int)page + usage:(unsigned int)usage + duration:(NSTimeInterval)duration + error:(NSError **)error +{ + id event = FBCreateXCDeviceEvent(page, usage, duration, error); + return nil == event ? NO : [self performDeviceEvent:event error:error]; +} + +- (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)error +{ + SEL selector = NSSelectorFromString(@"setAppearanceMode:"); + if (nil != selector && [self respondsToSelector:selector]) { + NSMethodSignature *signature = [self methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setTarget:self]; + [invocation setArgument:&appearance atIndex:2]; + [invocation invoke]; + return YES; + } + +#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3) + // Xcode 14.3.1 can build these values. + // For iOS 17+ + if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) { + self.appearance = (XCUIDeviceAppearance) appearance; + return YES; + } +#endif + + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Current Xcode SDK does not support appearance changing"] + buildError:error]; +} + +- (NSNumber *)fb_getAppearance +{ +#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3) + // Xcode 14.3.1 can build these values. + // For iOS 17+ + if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) { + return [NSNumber numberWithLongLong:[self appearance]]; + } +#endif + + return [self respondsToSelector:@selector(appearanceMode)] + ? [NSNumber numberWithLongLong:[self appearanceMode]] + : nil; +} + +#if !TARGET_OS_TV +- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error +{ + return [FBXCTestDaemonsProxy setSimulatedLocation:location error:error]; +} + +- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error +{ + return [FBXCTestDaemonsProxy getSimulatedLocation:error]; +} + +- (BOOL)fb_clearSimulatedLocation:(NSError **)error +{ + return [FBXCTestDaemonsProxy clearSimulatedLocation:error]; +} +#endif + +@end diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h new file mode 100644 index 0000000..b947fcd --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_TV +@interface XCUIDevice (FBRotation) + +/** + Sets requested device interface orientation. + + @param orientation The interface orientation. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation; + +/** + Sets the devices orientation to the rotation passed. + + @param rotationObj The rotation defining the devices orientation. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj; + +/*! The UIDeviceOrientation to rotation mappings */ +@property (strong, nonatomic, readonly) NSDictionary *fb_rotationMapping; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m new file mode 100644 index 0000000..164b23e --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIDevice+FBRotation.h" + +#import "FBConfiguration.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement+FBUtilities.h" + +# if !TARGET_OS_TV + +@implementation XCUIDevice (FBRotation) + +- (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation +{ + XCUIApplication *application = XCUIApplication.fb_activeApplication; + [XCUIDevice sharedDevice].orientation = orientation; + return [self waitUntilInterfaceIsAtOrientation:orientation application:application]; +} + +- (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj +{ + NSArray *keysForRotationObj = [self.fb_rotationMapping allKeysForObject:rotationObj]; + if (keysForRotationObj.count == 0) { + return NO; + } + NSInteger orientation = keysForRotationObj.firstObject.integerValue; + XCUIApplication *application = XCUIApplication.fb_activeApplication; + [XCUIDevice sharedDevice].orientation = orientation; + return [self waitUntilInterfaceIsAtOrientation:orientation application:application]; +} + +- (BOOL)waitUntilInterfaceIsAtOrientation:(NSInteger)orientation application:(XCUIApplication *)application +{ + // Tapping elements immediately after rotation may fail due to way UIKit is handling touches. + // We should wait till UI cools off, before continuing + [application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + + return application.interfaceOrientation == orientation; +} + +- (NSDictionary *)fb_rotationMapping +{ + static NSDictionary *rotationMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + rotationMap = + @{ + @(UIDeviceOrientationUnknown) : @{@"x" : @(-1), @"y" : @(-1), @"z" : @(-1)}, + @(UIDeviceOrientationPortrait) : @{@"x" : @(0), @"y" : @(0), @"z" : @(0)}, + @(UIDeviceOrientationPortraitUpsideDown) : @{@"x" : @(0), @"y" : @(0), @"z" : @(180)}, + @(UIDeviceOrientationLandscapeLeft) : @{@"x" : @(0), @"y" : @(0), @"z" : @(270)}, + @(UIDeviceOrientationLandscapeRight) : @{@"x" : @(0), @"y" : @(0), @"z" : @(90)}, + @(UIDeviceOrientationFaceUp) : @{@"x" : @(90), @"y" : @(0), @"z" : @(0)}, + @(UIDeviceOrientationFaceDown) : @{@"x" : @(270), @"y" : @(0), @"z" : @(0)}, + }; + }); + return rotationMap; +} + +@end +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h new file mode 100644 index 0000000..baa5656 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBAccessibility) + +/*! Whether or not the element is accessible */ +@property (atomic, readonly) BOOL fb_isAccessibilityElement; + +@end + + +@interface FBXCElementSnapshotWrapper (FBAccessibility) + +/*! Whether or not the element in snapshot is accessible */ +@property (atomic, readonly) BOOL fb_isAccessibilityElement; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m new file mode 100644 index 0000000..47d62e5 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBAccessibility.h" + +#import "FBConfiguration.h" +#import "XCTestPrivateSymbols.h" +#import "XCUIElement+FBUtilities.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" + +@implementation XCUIElement (FBAccessibility) + +- (BOOL)fb_isAccessibilityElement +{ + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isAccessibilityElement; +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBAccessibility) + +- (BOOL)fb_isAccessibilityElement +{ + NSNumber *isAccessibilityElement = self.additionalAttributes[FB_XCAXAIsElementAttribute]; + if (nil != isAccessibilityElement) { + return isAccessibilityElement.boolValue; + } + + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsElementAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsElementAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + return [attributeValue boolValue]; + } + + NSLog(@"Cannot determine accessibility of '%@' natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h new file mode 100644 index 0000000..119723a --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBCaching) + +@property (nonatomic, readonly) NSString *fb_cacheId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m new file mode 100644 index 0000000..c2e9f11 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBCaching.h" + +#import + +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBUID.h" + +@implementation XCUIElement (FBCaching) + +static char XCUIELEMENT_CACHE_ID_KEY; + +@dynamic fb_cacheId; + +- (NSString *)fb_cacheId +{ + id result = objc_getAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY); + if ([result isKindOfClass:NSString.class]) { + return (NSString *)result; + } + + NSString *uid = self.fb_uid; + objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return uid; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h new file mode 100644 index 0000000..0e56bfe --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBClassChain) + +/** + Returns an array of descendants matching given class chain query. + This query is similar to xpath, but can only include indexes, predicates and valid class names. Search by direct children and descendant elements is supported. Examples of direct search requests: + XCUIElementTypeWindow/XCUIElementTypeButton[3] - select the third child button of the first child window element. + XCUIElementTypeWindow - select all the children windows. + XCUIElementTypeWindow[2] - select the second child window in the hierarchy. Indexing starts at 1. + XCUIElementTypeWindow/XCUIElementTypeAny[3] - select the third child (of any type) of the first child. window + XCUIElementTypeWindow[2]/XCUIElementTypeAny - select all the children of the second child window. + XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2] - select the second last child of the second child window. + One may use '*' (star) character to substitute the universal 'XCUIElementTypeAny' class name. + XCUIElementTypeWindow[`name CONTAINS[cd] "blabla"`] - select all windows, where name attribute starts with "blabla" or "BlAbla". + XCUIElementTypeWindow[`label BEGINSWITH "blabla"`][-1] - select the last window, where label text begins with "blabla". + XCUIElementTypeWindow/XCUIElementTypeAny[`value == "bla1" OR label == "bla2"`] - select all children of the first window, where value is "bla1" or label is "bla2". + XCUIElementTypeWindow[`name == "you're the winner"`]/XCUIElementTypeAny[`visible == 1`] - select all visible children of the first window named "you're the winner". + XCUIElementTypeWindow/XCUIElementTypeTable/XCUIElementTypeCell[`visible == 1`][$type == XCUIElementTypeImage AND name == 'bla'$]/XCUIElementTypeTextField - select a text field, which is a direct child of a visible table cell, which has at least one descendant image with identifier 'bla'. + Predicate string should be always enclosed into ` or $ characters inside square brackets. Use `` or $$ to escape a single ` or $ character inside predicate expression. + Single backtick means the predicate expression is applied to the current children. It is the direct alternative of matchingPredicate: query selector. + Single dollar sign means the predicate expression is applied to all the descendants of the current element(s). It is the direct alternative of containingPredicate: query selector. + Predicate expression should be always put before the index, but never after it. All predicate expressions are executed in the same exact order, which is set in the chain query. + It is not recommended to set explicit indexes for intermediate chain elements, because it slows down the lookup speed. + + Indirect descendant search requests are pretty similar to requests above: + ** /XCUIElementTypeCell[`name BEGINSWITH "A"`][-1]/XCUIElementTypeButton[10] - select the 10-th child button of the very last cell in the tree, whose name starts with 'A'. + ** /XCUIElementTypeCell[`name BEGINSWITH "B"`] - select all cells in the tree, where name starts with 'B' + ** /XCUIElementTypeCell[`name BEGINSWITH "C"`]/XCUIElementTypeButton[10] - select the 10-th child button of the first cell in the tree, whose name starts with 'C' and which has at least ten direct children of type XCUIElementTypeButton. + ** /XCUIElementTypeCell[`name BEGINSWITH "D"`]/ ** /XCUIElementTypeButton - select the all descendant buttons of the first cell in the tree, whose name starts with 'D'. + + Double star and slash is the marker of the fact, that the next following item is the descendant of the previous chain item, rather than its child. + + The matching result is similar to what XCTest's children... and descendants... selector calls of XCUIElement class instances produce when combined into a chain. + + @param classChainQuery valid class chain query string + @param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be resolved and returned. + This will speed up the search significantly if the given chain matches multiple nodes in the UI tree + @return an array of descendants matching given class chain + @throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute(s) + */ +- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m new file mode 100644 index 0000000..88197ec --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBClassChain.h" + +#import "FBClassChainQueryParser.h" +#import "FBXCodeCompatibility.h" +#import "FBExceptions.h" + +@implementation XCUIElement (FBClassChain) + +- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + NSError *error; + FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:classChainQuery error:&error]; + if (nil == parsedChain) { + @throw [NSException exceptionWithName:FBClassChainQueryParseException reason:error.localizedDescription userInfo:error.userInfo]; + return nil; + } + NSMutableArray *lookupChain = parsedChain.elements.mutableCopy; + FBClassChainItem *chainItem = lookupChain.firstObject; + XCUIElement *currentRoot = self; + XCUIElementQuery *query = [currentRoot fb_queryWithChainItem:chainItem query:nil]; + [lookupChain removeObjectAtIndex:0]; + while (lookupChain.count > 0) { + BOOL isRootChanged = NO; + if (nil != chainItem.position) { + // It is necessary to resolve the query if intermediate element index is not zero or one, + // because predicates don't support search by indexes + NSArray *currentRootMatch = [self.class fb_matchingElementsWithItem:chainItem + query:query + shouldReturnAfterFirstMatch:nil]; + if (0 == currentRootMatch.count) { + return @[]; + } + currentRoot = currentRootMatch.firstObject; + isRootChanged = YES; + } + chainItem = [lookupChain firstObject]; + query = [currentRoot fb_queryWithChainItem:chainItem query:isRootChanged ? nil : query]; + [lookupChain removeObjectAtIndex:0]; + } + return [self.class fb_matchingElementsWithItem:chainItem + query:query + shouldReturnAfterFirstMatch:@(shouldReturnAfterFirstMatch)]; +} + +- (XCUIElementQuery *)fb_queryWithChainItem:(FBClassChainItem *)item query:(nullable XCUIElementQuery *)query +{ + if (item.isDescendant) { + if (query) { + query = [query descendantsMatchingType:item.type]; + } else { + query = [self.fb_query descendantsMatchingType:item.type]; + } + } else { + if (query) { + query = [query childrenMatchingType:item.type]; + } else { + query = [self.fb_query childrenMatchingType:item.type]; + } + } + if (item.predicates) { + for (FBAbstractPredicateItem *predicate in item.predicates) { + if ([predicate isKindOfClass:FBSelfPredicateItem.class]) { + query = [query matchingPredicate:predicate.value]; + } else if ([predicate isKindOfClass:FBDescendantPredicateItem.class]) { + query = [query containingPredicate:predicate.value]; + } + } + } + return query; +} + ++ (NSArray *)fb_matchingElementsWithItem:(FBClassChainItem *)item query:(XCUIElementQuery *)query shouldReturnAfterFirstMatch:(nullable NSNumber *)shouldReturnAfterFirstMatch +{ + if (1 == item.position.integerValue || (0 == item.position.integerValue && shouldReturnAfterFirstMatch.boolValue)) { + XCUIElement *result = query.fb_firstMatch; + return result ? @[result] : @[]; + } + NSArray *allMatches = query.fb_allMatches; + if (0 == item.position.integerValue) { + return allMatches; + } + if (allMatches.count >= (NSUInteger)ABS(item.position.integerValue)) { + return item.position.integerValue > 0 + ? @[[allMatches objectAtIndex:item.position.integerValue - 1]] + : @[[allMatches objectAtIndex:allMatches.count + item.position.integerValue]]; + } + return @[]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBFind.h b/WebDriverAgentLib/Categories/XCUIElement+FBFind.h new file mode 100644 index 0000000..8a91fcd --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBFind.h @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBFind) + +/** + Returns an array of descendants matching given class name + + @param className requested class name + @param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be + resolved and returned. This will speed up the search significantly if given class name matches multiple + nodes in the UI tree + @return an array of descendants matching given class name + */ +- (NSArray *)fb_descendantsMatchingClassName:(NSString *)className shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; + +/** + Returns an array of descendants matching given accessibility id + + @param accessibilityId requested accessibility id + @param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be + resolved and returned. This will speed up the search significantly if given id matches multiple + nodes in the UI tree + @return an array of descendants matching given accessibility id + */ +- (NSArray *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; + +/** + Returns an array of descendants matching given xpath query + + @param xpathQuery requested xpath query + @param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be + resolved and returned. This will speed up the search significantly if given xpath matches multiple + nodes in the UI tree + @return an array of descendants matching given xpath query + */ +- (NSArray *)fb_descendantsMatchingXPathQuery:(NSString *)xpathQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; + +/** + Returns an array of descendants matching given predicate. + Allowed property names are only these declared in FBElement protocol (property names are received in runtime) + and their shortcuts (without 'wd' prefix). All other property names are considered as unknown. + + @param predicate requested predicate + @param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be + resolved and returned. This will speed up the search significantly if given predicate matches multiple + nodes in the UI tree + @return an array of descendants matching given predicate + @throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol + */ +- (NSArray *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; + +/** + Returns an array of descendants with property matching given value + + @param property requested property name + @param value requested value of the property + @param partialSearch determines whether it should be exact or partial match + @return an array of descendants with property matching given value + */ +- (NSArray *)fb_descendantsMatchingProperty:(NSString *)property value:(NSString *)value partialSearch:(BOOL)partialSearch; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBFind.m b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m new file mode 100644 index 0000000..ceab0ad --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +#import "XCUIElement+FBFind.h" + +#import "FBMacros.h" +#import "FBElementTypeTransformer.h" +#import "FBConfiguration.h" +#import "NSPredicate+FBFormat.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElementQuery.h" +#import "FBElementUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXPath.h" + +@implementation XCUIElement (FBFind) + ++ (NSArray *)fb_extractMatchingElementsFromQuery:(XCUIElementQuery *)query + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + if (!shouldReturnAfterFirstMatch) { + return query.fb_allMatches; + } + XCUIElement *matchedElement = query.fb_firstMatch; + return matchedElement ? @[matchedElement] : @[]; +} + +- (id)fb_cachedSnapshotWithQuery:(XCUIElementQuery *)query +{ + return [self isKindOfClass:XCUIApplication.class] ? query.rootElementSnapshot : self.fb_cachedSnapshot; +} + +#pragma mark - Search by ClassName + +- (NSArray *)fb_descendantsMatchingClassName:(NSString *)className + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + XCUIElementType type = [FBElementTypeTransformer elementTypeWithTypeName:className]; + XCUIElementQuery *query = [self.fb_query descendantsMatchingType:type]; + NSMutableArray *result = [NSMutableArray array]; + [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]]; + id cachedSnapshot = [self fb_cachedSnapshotWithQuery:query]; + if (type == XCUIElementTypeAny || cachedSnapshot.elementType == type) { + if (shouldReturnAfterFirstMatch || result.count == 0) { + return @[self]; + } + [result insertObject:self atIndex:0]; + } + return result.copy; +} + + +#pragma mark - Search by property value + +- (NSArray *)fb_descendantsMatchingProperty:(NSString *)property + value:(NSString *)value + partialSearch:(BOOL)partialSearch +{ + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:(partialSearch ? @"%K CONTAINS %@" : @"%K == %@"), property, value]; + return [self fb_descendantsMatchingPredicate:searchPredicate shouldReturnAfterFirstMatch:NO]; +} + +#pragma mark - Search by Predicate String + +- (NSArray *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:predicate]; + XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:formattedPredicate]; + NSMutableArray *result = [NSMutableArray array]; + [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]]; + id cachedSnapshot = [self fb_cachedSnapshotWithQuery:query]; + // Include self element into predicate search + if ([formattedPredicate evaluateWithObject:cachedSnapshot]) { + if (shouldReturnAfterFirstMatch || result.count == 0) { + return @[self]; + } + [result insertObject:self atIndex:0]; + } + return result.copy; +} + + +#pragma mark - Search by xpath + +- (NSArray *)fb_descendantsMatchingXPathQuery:(NSString *)xpathQuery + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + // XPath will try to match elements only class name, so requesting elements by XCUIElementTypeAny will not work. We should use '*' instead. + xpathQuery = [xpathQuery stringByReplacingOccurrencesOfString:@"XCUIElementTypeAny" withString:@"*"]; + NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self forQuery:xpathQuery]; + if (0 == [matchingSnapshots count]) { + return @[]; + } + if (shouldReturnAfterFirstMatch) { + id snapshot = matchingSnapshots.firstObject; + matchingSnapshots = @[snapshot]; + } + XCUIElement *scopeRoot = FBConfiguration.limitXpathContextScope ? self : self.application; + return [scopeRoot fb_filterDescendantsWithSnapshots:matchingSnapshots + onlyChildren:NO]; +} + + +#pragma mark - Search by Accessibility Id + +- (NSArray *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, + NSDictionary * _Nullable bindings) { + @autoreleasepool { + return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; + } + }]; + return [self fb_descendantsMatchingPredicate:predicate + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h new file mode 100644 index 0000000..8005b22 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_TV + +@interface XCUIElement (FBForceTouch) + +/** + Performs force touch on element + + @param relativeCoordinate hit point coordinate relative to the current element position. + nil value means to use the default element hit point + @param pressure The pressure of the force touch – valid values are [0, touch.maximumPossibleForce] + nil value would use the default pressure value + @param duration The duration of the gesture in float seconds + nil value would use the default duration value + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_forceTouchCoordinate:(nullable NSValue *)relativeCoordinate + pressure:(nullable NSNumber *)pressure + duration:(nullable NSNumber *)duration + error:(NSError **)error; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m new file mode 100644 index 0000000..bd5d0bd --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBForceTouch.h" + +#if !TARGET_OS_TV + +#import "FBErrorBuilder.h" +#import "XCUICoordinate.h" +#import "XCUIDevice.h" + +@implementation XCUIElement (FBForceTouch) + +- (BOOL)fb_forceTouchCoordinate:(NSValue *)relativeCoordinate + pressure:(NSNumber *)pressure + duration:(NSNumber *)duration + error:(NSError **)error +{ + if (![XCUIDevice sharedDevice].supportsPressureInteraction) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Force press is not supported on this device"] + buildError:error]; + } + + if (nil == relativeCoordinate) { + if (nil == pressure || nil == duration) { + [self forcePress]; + } else { + [self pressWithPressure:[pressure doubleValue] duration:[duration doubleValue]]; + } + } else { + CGVector offset = CGVectorMake(relativeCoordinate.CGPointValue.x, + relativeCoordinate.CGPointValue.y); + XCUICoordinate *hitPoint = [[self coordinateWithNormalizedOffset:CGVectorMake(0, 0)] + coordinateWithOffset:offset]; + if (nil == pressure || nil == duration) { + [hitPoint forcePress]; + } else { + [hitPoint pressWithPressure:[pressure doubleValue] duration:[duration doubleValue]]; + } + } + return YES; +} + +@end + +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h new file mode 100644 index 0000000..fd17acc --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBIsVisible) + +/*! Whether or not the element is visible */ +@property (atomic, readonly) BOOL fb_isVisible; + +@end + + +@interface FBXCElementSnapshotWrapper (FBIsVisible) + +/*! Whether or not the element is visible */ +@property (atomic, readonly) BOOL fb_isVisible; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m new file mode 100644 index 0000000..75a228a --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBIsVisible.h" + +#import "FBElementUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBVisibleFrame.h" +#import "XCTestPrivateSymbols.h" + +NSNumber* _Nullable fetchSnapshotVisibility(id snapshot) +{ + return nil == snapshot.additionalAttributes ? nil : snapshot.additionalAttributes[FB_XCAXAIsVisibleAttribute]; +} + +@implementation XCUIElement (FBIsVisible) + +- (BOOL)fb_isVisible +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible; + } +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBIsVisible) + +- (BOOL)fb_hasVisibleDescendants +{ + for (id descendant in (self._allDescendants ?: @[])) { + if ([fetchSnapshotVisibility(descendant) boolValue]) { + return YES; + } + } + return NO; +} + +- (BOOL)fb_isVisible +{ + NSNumber *isVisible = fetchSnapshotVisibility(self); + if (nil != isVisible) { + return isVisible.boolValue; + } + + // Fetching the attribute value is expensive. + // Shortcircuit here to save time and assume if any of descendants + // is already determined as visible then the container should be visible as well + if ([self fb_hasVisibleDescendants]) { + return YES; + } + + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsVisibleAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsVisibleAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + @autoreleasepool { + return [attributeValue boolValue]; + } + } + + NSLog(@"Cannot determine visiblity of %@ natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h new file mode 100644 index 0000000..0873a65 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +@interface FBXCElementSnapshotWrapper (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m new file mode 100644 index 0000000..8751490 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBLogger.h" +#import "XCUIElement+FBMinMax.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@interface FBXCElementSnapshotWrapper (FBMinMaxInternal) + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol; + +@end + +@implementation XCUIElement (FBMinMax) + +- (NSNumber *)fb_minValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_minValue]; + } +} + +- (NSNumber *)fb_maxValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_maxValue]; + } +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBMinMax) + +- (NSNumber *)fb_minValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMinValueAttributeName + symbol:FB_XCAXACustomMinValueAttribute]; +} + +- (NSNumber *)fb_maxValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMaxValueAttributeName + symbol:FB_XCAXACustomMaxValueAttribute]; +} + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol +{ + NSNumber *cached = (self.snapshot.additionalAttributes ?: @{})[symbol]; + if (cached) { + return cached; + } + + NSError *error = nil; + NSNumber *raw = [self fb_attributeValue:attributeName error:&error]; + if (nil != raw) { + NSMutableDictionary *updated = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + updated[symbol] = raw; + self.snapshot.additionalAttributes = updated.copy; + return raw; + } + + [FBLogger logFmt:@"[FBMinMax] Cannot determine %@ for %@: %@", attributeName, self.fb_description, error.localizedDescription]; + return nil; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h new file mode 100644 index 0000000..72762a3 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBPickerWheel) + +/** + Selects the next available option in Picker Wheel + + @param offset the value in range [0.01, 0.5]. It defines how far from picker + wheel's center the click should happen. The actual distance is culculated by + multiplying this value to the actual picker wheel height. Too small offset value + may not change the picker wheel value and too high value may cause the wheel to switch + two or more values at once. Usually the optimal value is located in range [0.15, 0.3] + @param error returns error object if there was an error while selecting the + next picker value + @return YES if the current option has been successfully switched. Otherwise NO + */ +- (BOOL)fb_selectNextOptionWithOffset:(CGFloat)offset error:(NSError **)error; + +/** + Selects the previous available option in Picker Wheel + + @param offset the value in range [0.01, 0.5]. It defines how far from picker + wheel's center the click should happen. The actual distance is culculated by + multiplying this value to the actual picker wheel height. Too small offset value + may not change the picker wheel value and too high value may cause the wheel to switch + two or more values at once. Usually the optimal value is located in range [0.15, 0.3] + @param error returns error object if there was an error while selecting the + previous picker value + @return YES if the current option has been successfully switched. Otherwise NO + */ +- (BOOL)fb_selectPreviousOptionWithOffset:(CGFloat)offset error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m new file mode 100644 index 0000000..5518921 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBPickerWheel.h" + +#import "FBRunLoopSpinner.h" +#import "FBXCElementSnapshot.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUID.h" +#import "XCUICoordinate.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBResolve.h" + +#if !TARGET_OS_TV +@implementation XCUIElement (FBPickerWheel) + +static const NSTimeInterval VALUE_CHANGE_TIMEOUT = 2; + +- (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)error +{ + id snapshot = [self fb_standardSnapshot]; + NSString *previousValue = snapshot.value; + XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; + XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)]; + // If picker value is reflected in its accessiblity id + // then fetching of the next snapshot may fail with StaleElementReferenceError + // because we bound elements by their accessbility ids by default. + // Fetching stable instance of an element allows it to be bounded to the + // unique element identifier (UID), so it could be found next time even if its + // id is different from the initial one. See https://github.com/appium/appium/issues/17569 + XCUIElement *stableInstance = [self fb_stableInstanceWithUid:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]]; + [endCoord tap]; + return [[[[FBRunLoopSpinner new] + timeout:VALUE_CHANGE_TIMEOUT] + timeoutErrorMessage:[NSString stringWithFormat:@"Picker wheel value has not been changed after %@ seconds timeout", @(VALUE_CHANGE_TIMEOUT)]] + spinUntilTrue:^BOOL{ + return ![stableInstance.value isEqualToString:previousValue]; + } + error:error]; +} + +- (BOOL)fb_selectNextOptionWithOffset:(CGFloat)offset error:(NSError **)error +{ + return [self fb_scrollWithOffset:offset error:error]; +} + +- (BOOL)fb_selectPreviousOptionWithOffset:(CGFloat)offset error:(NSError **)error +{ + return [self fb_scrollWithOffset:-offset error:error]; +} + +@end +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h new file mode 100644 index 0000000..09b174f --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBResolve) + +/*! This property is always true unless the element gets resolved by its internal UUID (e.g. results of an xpath query) */ +@property (nullable, nonatomic) NSNumber *fb_isResolvedNatively; + +/** + Returns element instance based on query by element's UUID rather than any other attributes, which + might be a subject of change during the application life cycle. The UUID is calculated based on the PID + of the application to which this particular element belongs and the identifier of the underlying AXElement + instance. That usually guarantees the same element is always going to be matched in scope of the parent + application independently of its current attribute values. + Example: We have an element X with value Y. Our locator looks like 'value == Y'. Normally, if the element's + value is changed to Z and we try to reuse this cached instance of it then a StaleElement error is thrown. + Although, if the cached element instance is the one returned by this API call then the same element + is going to be matched and no staleness exception will be thrown. + + @param uid Element UUID + @return Either the same element instance if `fb_isResolvedNatively` was set to NO (usually the cache for elements + matched by xpath locators) or the stable instance of the self element based on the query by element's UUID. + */ +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *__nullable)uid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m new file mode 100644 index 0000000..f288114 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBResolve.h" + +#import + +#import "XCUIElement.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUID.h" + +@implementation XCUIElement (FBResolve) + +static char XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY; + +@dynamic fb_isResolvedNatively; + +- (void)setFb_isResolvedNatively:(NSNumber *)isResolvedNatively +{ + objc_setAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY, isResolvedNatively, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSNumber *)fb_isResolvedNatively +{ + NSNumber *result = objc_getAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY); + return nil == result ? @YES : result; +} + +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *)uid +{ + if (nil == uid || ![self.fb_isResolvedNatively boolValue] || [self isKindOfClass:XCUIApplication.class]) { + return self; + } + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@", FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; + @autoreleasepool { + XCUIElementQuery *query = [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; + XCUIElement *result = [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject; + if (nil != result) { + result.fb_isResolvedNatively = @NO; + return result; + } + } + return self; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h new file mode 100644 index 0000000..416544e --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Defines directions in which scrolling is possible. + */ +typedef NS_ENUM(NSUInteger, FBXCUIElementScrollDirection) { + FBXCUIElementScrollDirectionUnknown, + FBXCUIElementScrollDirectionVertical, + FBXCUIElementScrollDirectionHorizontal, +}; + +#if !TARGET_OS_TV + +@interface XCUIElement (FBScrolling) + +/** + Scrolls receiver up by one screen height + + @param distance Normalized <0.0 - 1.0> scroll distance distance + */ +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance; + +/** + Scrolls receiver down by one screen height + + @param distance Normalized <0.0 - 1.0> scroll distance distance + */ +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance; + +/** + Scrolls receiver left by one screen width + + @param distance Normalized <0.0 - 1.0> scroll distance distance + */ +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance; + +/** + Scrolls receiver right by one screen width + + @param distance Normalized <0.0 - 1.0> scroll distance distance + */ +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance; + +/** + Scrolls parent scroll view till receiver is visible. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_scrollToVisibleWithError:(NSError **)error; + +/** + Scrolls parent scroll view till the current element is visible. + This call is fast as it uses a native XCTest implementation. + The element must be hittable. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error; + +/** + Scrolls parent scroll view till receiver is visible. Whenever element is invisible it scrolls by normalizedScrollDistance + in its direction. E.g. if normalizedScrollDistance is equal to 0.5, each step will scroll by half of scroll view's size. + + @param normalizedScrollDistance single scroll step normalized (0.0 - 1.0) distance + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance error:(NSError **)error; + +/** + Scrolls parent scroll view till receiver is visible. Whenever element is invisible it scrolls by normalizedScrollDistance + in its direction. E.g. if normalizedScrollDistance is equal to 0.5, each step will scroll by half of scroll view's size. + + @param normalizedScrollDistance single scroll step normalized (0.0 - 1.0) distance + @param scrollDirection the direction in which the scroll view should be scrolled, or FBXCUIElementScrollDirectionUnknown + to attempt to determine it automatically + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance scrollDirection:(FBXCUIElementScrollDirection)scrollDirection error:(NSError **)error; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m new file mode 100644 index 0000000..6b63d0a --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m @@ -0,0 +1,342 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBScrolling.h" + +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIApplication.h" +#import "XCUICoordinate.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBVisibleFrame.h" +#import "XCUIElement.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCTestPrivateSymbols.h" + +const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch +const CGFloat FBScrollToVisibleNormalizedDistance = .5f; +const CGFloat FBTouchEventDelay = 0.5f; +const CGFloat FBTouchVelocity = 300; // pixels per sec +const CGFloat FBScrollTouchProportion = 0.75f; + +#if !TARGET_OS_TV + +@interface FBXCElementSnapshotWrapper (FBScrolling) + +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application; +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application; +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application; +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application; +- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application; +- (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error; + +@end + +@implementation XCUIElement (FBScrolling) + +- (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error +{ + id snapshot = [self fb_customSnapshot]; + return nil != [self _hitPointByAttemptingToScrollToVisibleSnapshot:snapshot + error:error]; +} + +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance +{ + id snapshot = [self fb_customSnapshot]; + [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance + inApplication:self.application]; +} + +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance +{ + id snapshot = [self fb_customSnapshot]; + [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance + inApplication:self.application]; +} + +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance +{ + id snapshot = [self fb_customSnapshot]; + [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance + inApplication:self.application]; +} + +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance +{ + id snapshot = [self fb_customSnapshot]; + [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance + inApplication:self.application]; +} + +- (BOOL)fb_scrollToVisibleWithError:(NSError **)error +{ + return [self fb_scrollToVisibleWithNormalizedScrollDistance:FBScrollToVisibleNormalizedDistance error:error]; +} + +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance + error:(NSError **)error +{ + return [self fb_scrollToVisibleWithNormalizedScrollDistance:normalizedScrollDistance + scrollDirection:FBXCUIElementScrollDirectionUnknown + error:error]; +} + +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance + scrollDirection:(FBXCUIElementScrollDirection)scrollDirection + error:(NSError **)error +{ + FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; + + if (prescrollSnapshot.isWDVisible) { + return YES; + } + + static dispatch_once_t onceToken; + static NSArray *acceptedParents; + dispatch_once(&onceToken, ^{ + acceptedParents = @[ + @(XCUIElementTypeScrollView), + @(XCUIElementTypeCollectionView), + @(XCUIElementTypeTable), + @(XCUIElementTypeWebView), + ]; + }); + + __block NSArray> *cellSnapshots; + __block NSMutableArray> *visibleCellSnapshots = [NSMutableArray new]; + id scrollView = [prescrollSnapshot fb_parentMatchingOneOfTypes:acceptedParents + filter:^(id snapshot) { + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + + if (![wrappedSnapshot isWDVisible]) { + return NO; + } + + cellSnapshots = [wrappedSnapshot fb_descendantsCellSnapshots]; + + for (id cellSnapshot in cellSnapshots) { + FBXCElementSnapshotWrapper *wrappedCellSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:cellSnapshot]; + if (wrappedCellSnapshot.wdVisible) { + [visibleCellSnapshots addObject:cellSnapshot]; + if (visibleCellSnapshots.count > 1) { + return YES; + } + } + } + + return NO; + }]; + + if (scrollView == nil) { + return + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Failed to find scrollable visible parent with 2 visible children"] + buildError:error]; + } + + id targetCellSnapshot = [prescrollSnapshot fb_parentCellSnapshot]; + id lastSnapshot = visibleCellSnapshots.lastObject; + // Can't just do indexOfObject, because targetCellSnapshot may represent the same object represented by a member of cellSnapshots, yet be a different object + // than that member. This reflects the fact that targetCellSnapshot came out of self.fb_parentCellSnapshot, not out of cellSnapshots directly. + // If the result is NSNotFound, we'll just proceed by scrolling downward/rightward, since NSNotFound will always be larger than the current index. + NSUInteger targetCellIndex = [cellSnapshots indexOfObjectPassingTest:^BOOL(id _Nonnull obj, + NSUInteger idx, BOOL *_Nonnull stop) { + return [obj _matchesElement:targetCellSnapshot]; + }]; + NSUInteger visibleCellIndex = [cellSnapshots indexOfObject:lastSnapshot]; + + if (scrollDirection == FBXCUIElementScrollDirectionUnknown) { + // Try to determine the scroll direction by determining the vector between the first and last visible cells + id firstVisibleCell = visibleCellSnapshots.firstObject; + id lastVisibleCell = visibleCellSnapshots.lastObject; + CGVector cellGrowthVector = CGVectorMake(firstVisibleCell.frame.origin.x - lastVisibleCell.frame.origin.x, + firstVisibleCell.frame.origin.y - lastVisibleCell.frame.origin.y + ); + if (ABS(cellGrowthVector.dy) > ABS(cellGrowthVector.dx)) { + scrollDirection = FBXCUIElementScrollDirectionVertical; + } else { + scrollDirection = FBXCUIElementScrollDirectionHorizontal; + } + } + + const NSUInteger maxScrollCount = 25; + NSUInteger scrollCount = 0; + FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView]; + // Scrolling till cell is visible and get current value of frames + while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) { + @autoreleasepool { + if (targetCellIndex < visibleCellIndex) { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance + inApplication:self.application] : + [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + else { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance + inApplication:self.application] : + [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + scrollCount++; + // Wait for scroll animation + [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + } + } + + if (scrollCount >= maxScrollCount) { + return + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Failed to perform scroll with visible cell due to max scroll count reached"] + buildError:error]; + } + + // Cell is now visible, but it might be only partialy visible, scrolling till whole frame is visible. + // Sometimes, attempting to grab the parent snapshot of the target cell after scrolling is complete causes a stale element reference exception. + // Trying fb_cachedSnapshot first + FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; + targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot]; + CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame; + + CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width, + visibleFrame.size.height - targetCellSnapshot.frame.size.height + ); + return [scrollViewWrapped fb_scrollByVector:scrollVector + inApplication:self.application + error:error]; +} + +- (BOOL)fb_isEquivalentElementSnapshotVisible:(id)snapshot +{ + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + + if (wrappedSnapshot.isWDVisible) { + return YES; + } + + id appSnapshot = [self.application fb_standardSnapshot]; + for (id elementSnapshot in appSnapshot._allDescendants.copy) { + FBXCElementSnapshotWrapper *wrappedElementSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:elementSnapshot]; + // We are comparing pre-scroll snapshot so frames are irrelevant. + if ([wrappedSnapshot fb_framelessFuzzyMatchesElement:elementSnapshot] + && wrappedElementSnapshot.isWDVisible) { + return YES; + } + } + return NO; +} + +@end + + +@implementation FBXCElementSnapshotWrapper (FBScrolling) + +- (CGRect)scrollingFrame +{ + return self.visibleFrame; +} + +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application +{ + [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application]; +} + +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application +{ + [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application]; +} + +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application +{ + [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application]; +} + +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application +{ + [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application]; +} + +- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector + inApplication:(XCUIApplication *)application +{ + CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx, + CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy + ); + return [self fb_scrollByVector:scrollVector inApplication:application error:nil]; +} + +- (BOOL)fb_scrollByVector:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error +{ + CGVector scrollBoundingVector = CGVectorMake( + CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion, + CGRectGetHeight(self.scrollingFrame) * FBScrollTouchProportion + ); + scrollBoundingVector.dx = (CGFloat)floor(copysign(scrollBoundingVector.dx, vector.dx)); + scrollBoundingVector.dy = (CGFloat)floor(copysign(scrollBoundingVector.dy, vector.dy)); + + NSInteger preciseScrollAttemptsCount = 20; + CGVector CGZeroVector = CGVectorMake(0, 0); + BOOL shouldFinishScrolling = NO; + while (!shouldFinishScrolling) { + CGVector scrollVector = CGVectorMake(fabs(vector.dx) > fabs(scrollBoundingVector.dx) ? scrollBoundingVector.dx : vector.dx, + fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy); + vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy); + shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0; + if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector inApplication:application error:error]){ + return NO; + } + } + return YES; +} + +- (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector +{ + CGFloat x = CGRectGetMinX(self.scrollingFrame) + CGRectGetWidth(self.scrollingFrame) * (scrollingVector.dx < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion)); + CGFloat y = CGRectGetMinY(self.scrollingFrame) + CGRectGetHeight(self.scrollingFrame) * (scrollingVector.dy < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion)); + return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y)); +} + +- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error +{ + CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector]; + + XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0.0, 0.0)]; + XCUICoordinate *startCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:hitpointOffset]; + XCUICoordinate *endCoordinate = [[XCUICoordinate alloc] initWithCoordinate:startCoordinate pointsOffset:vector]; + + if (FBPointFuzzyEqualToPoint(startCoordinate.screenPoint, endCoordinate.screenPoint, FBFuzzyPointThreshold)) { + return YES; + } + + [startCoordinate pressForDuration:FBTouchEventDelay + thenDragToCoordinate:endCoordinate + withVelocity:FBTouchVelocity + thenHoldForDuration:FBTouchEventDelay]; + return YES; +} + +@end + +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h new file mode 100644 index 0000000..4a2b5c1 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBSwiping) + +/** + * Performs swipe gesture on the element + * + * @param direction Swipe direction. The following values are supported: up, down, left and right + * @param velocity Swipe speed in pixels per second + */ +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity; + +@end + +#if !TARGET_OS_TV +@interface XCUICoordinate (FBSwiping) + +/** + * Performs swipe gesture on the coordinate + * + * @param direction Swipe direction. The following values are supported: up, down, left and right + * @param velocity Swipe speed in pixels per second + */ +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m new file mode 100644 index 0000000..8f64b48 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBSwiping.h" + +#import "FBLogger.h" +#import "XCUIElement.h" + +void swipeWithDirection(NSObject *target, NSString *direction, NSNumber* _Nullable velocity) { + double velocityValue = .0; + if (nil != velocity) { + velocityValue = [velocity doubleValue]; + } + + if (velocityValue > 0) { + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:", + direction.lowercaseString.capitalizedString]); + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setArgument:&velocityValue atIndex:2]; + [invocation invokeWithTarget:target]; + } else { + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@", + direction.lowercaseString.capitalizedString]); + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:target]; + } +} + +@implementation XCUIElement (FBSwiping) + +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity +{ + swipeWithDirection(self, direction, velocity); +} + +@end + +#if !TARGET_OS_TV +@implementation XCUICoordinate (FBSwiping) + +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity +{ + swipeWithDirection(self, direction, velocity); +} + +@end +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h new file mode 100644 index 0000000..0a3bc07 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if TARGET_OS_TV +@interface XCUIElement (FBTVFocuse) + +/** + Sets focus + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_setFocusWithError:(NSError**) error; + +/** + Select a focused element + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_selectWithError:(NSError**) error; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m new file mode 100644 index 0000000..5aa8508 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBTVFocuse.h" + +#import +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" + +#if TARGET_OS_TV + +int const MAX_ITERATIONS_COUNT = 100; + +@implementation XCUIElement (FBTVFocuse) + +- (BOOL)fb_setFocusWithError:(NSError**) error +{ + [XCUIApplication.fb_activeApplication fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + + if (!self.wdEnabled) { + if (error) { + *error = [[FBErrorBuilder.builder withDescription: + [NSString stringWithFormat:@"'%@' element cannot be focused because it is disabled", self.description]] build]; + } + return NO; + } + + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:self]; + for (int i = 0; i < MAX_ITERATIONS_COUNT; i++) { + // Here hasFocus works so far. Maybe, it is because it is handled by `XCUIRemote`... + if (self.hasFocus) { + return YES; + } + + if (!self.exists) { + if (error) { + *error = [[FBErrorBuilder.builder withDescription: + [NSString stringWithFormat:@"'%@' element is not reachable because it does not exist. Try to use XCUIRemote commands.", self.description]] build]; + } + return NO; + } + + FBTVDirection direction = tracker.directionToFocusedElement; + if (direction != FBTVDirectionNone) { + [[XCUIRemote sharedRemote] pressButton: (XCUIRemoteButton)direction]; + } + } + + return NO; +} + +- (BOOL)fb_selectWithError:(NSError**) error +{ + BOOL result = [self fb_setFocusWithError: error]; + if (result) { + [[XCUIRemote sharedRemote] pressButton:XCUIRemoteButtonSelect]; + } + return result; +} +@end + +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h new file mode 100644 index 0000000..0b77558 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Types a text into the currently focused element. + + @param text text that should be typed + @param typingSpeed Frequency of typing (letters per sec) + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error); + +@interface XCUIElement (FBTyping) + +/** + Types a text into element. + It will try to activate keyboard on element, if element has no keyboard focus. + + @param text text that should be typed + @param shouldClear Whether to clear the input field before start typing + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_typeText:(NSString *)text + shouldClear:(BOOL)shouldClear + error:(NSError **)error; + +/** + Types a text into element. + It will try to activate keyboard on element, if element has no keyboard focus. + + @param text text that should be typed + @param shouldClear Whether to clear the input field before start typing + @param frequency Frequency of typing (letters per sec) + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_typeText:(NSString *)text + shouldClear:(BOOL)shouldClear + frequency:(NSUInteger)frequency + error:(NSError **)error; + +/** + Clears text on element. + It will try to activate keyboard on element, if element has no keyboard focus. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)fb_clearTextWithError:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m new file mode 100644 index 0000000..1d76f6c --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBTyping.h" + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBXCElementSnapshotWrapper.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "NSString+FBVisualLength.h" +#import "XCUIDevice+FBHelpers.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUtilities.h" +#import "XCSynthesizedEventRecord.h" +#import "XCPointerEventPath.h" + +#define MAX_TEXT_ABBR_LEN 12 +#define MAX_CLEAR_RETRIES 3 + +BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error) +{ + NSString *name = text.length <= MAX_TEXT_ABBR_LEN + ? [NSString stringWithFormat:@"Type '%@'", text] + : [NSString stringWithFormat:@"Type '%@…'", [text substringToIndex:MAX_TEXT_ABBR_LEN]]; + XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:name]; + XCPointerEventPath *ep = [[XCPointerEventPath alloc] initForTextInput]; + [ep typeText:text + atOffset:0.0 + typingSpeed:typingSpeed + shouldRedact:NO]; + [eventRecord addPointerEventPath:ep]; + return [FBXCTestDaemonsProxy synthesizeEventWithRecord:eventRecord error:error]; +} + +@interface NSString (FBRepeat) + +- (NSString *)fb_repeatTimes:(NSUInteger)times; + +@end + +@implementation NSString (FBRepeat) + +- (NSString *)fb_repeatTimes:(NSUInteger)times { + return [@"" stringByPaddingToLength:times * self.length + withString:self + startingAtIndex:0]; +} + +@end + + +@interface FBXCElementSnapshotWrapper (FBKeyboardFocus) + +- (BOOL)fb_hasKeyboardFocus; + +@end + +@implementation FBXCElementSnapshotWrapper (FBKeyboardFocus) + +- (BOOL)fb_hasKeyboardFocus +{ + // https://developer.apple.com/documentation/xctest/xcuielement/1500968-typetext?language=objc + // > The element or a descendant must have keyboard focus; otherwise an error is raised. + return self.hasKeyboardFocus || [self descendantsByFilteringWithBlock:^BOOL(id snapshot) { + return snapshot.hasKeyboardFocus; + }].count > 0; +} + +@end + + +@implementation XCUIElement (FBTyping) + +- (void)fb_prepareForTextInputWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot +{ + if (snapshot.fb_hasKeyboardFocus) { + return; + } + + [FBLogger logFmt:@"Neither the \"%@\" element itself nor its accessible descendants have the keyboard input focus", snapshot.fb_description]; +// There is no possibility to open the keyboard by tapping a field in TvOS +#if !TARGET_OS_TV + [FBLogger logFmt:@"Trying to tap the \"%@\" element to have it focused", snapshot.fb_description]; + [self tap]; + // It might take some time to update the UI + [self fb_standardSnapshot]; +#endif +} + +- (BOOL)fb_typeText:(NSString *)text + shouldClear:(BOOL)shouldClear + error:(NSError **)error +{ + return [self fb_typeText:text + shouldClear:shouldClear + frequency:FBConfiguration.maxTypingFrequency + error:error]; +} + +- (BOOL)fb_typeText:(NSString *)text + shouldClear:(BOOL)shouldClear + frequency:(NSUInteger)frequency + error:(NSError **)error +{ + id snapshot = [self fb_standardSnapshot]; + FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + [self fb_prepareForTextInputWithSnapshot:wrapped]; + if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) { + return NO; + } + return FBTypeText(text, frequency, error); +} + +- (BOOL)fb_clearTextWithError:(NSError **)error +{ + id snapshot = [self fb_standardSnapshot]; + return [self fb_clearTextWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot] + shouldPrepareForInput:YES + error:error]; +} + +- (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot + shouldPrepareForInput:(BOOL)shouldPrepareForInput + error:(NSError **)error +{ + id currentValue = snapshot.value; + if (nil != currentValue && ![currentValue isKindOfClass:NSString.class]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description] + buildError:error]; + } + + if (nil == currentValue || 0 == [currentValue fb_visualLength]) { + // Short circuit if the content is not present + return YES; + } + + static NSString *backspaceDeleteSequence; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + backspaceDeleteSequence = [[NSString alloc] initWithData:(NSData *)[@"\\u0008\\u007F" dataUsingEncoding:NSASCIIStringEncoding] + encoding:NSNonLossyASCIIStringEncoding]; + }); + + NSUInteger preClearTextLength = [currentValue fb_visualLength]; + NSString *backspacesToType = [backspaceDeleteSequence fb_repeatTimes:preClearTextLength]; + +#if TARGET_OS_IOS + NSUInteger retry = 0; + NSString *placeholderValue = snapshot.placeholderValue; + do { + // the ios needs to have keyboard focus to clear text + if (shouldPrepareForInput && 0 == retry) { + [self fb_prepareForTextInputWithSnapshot:snapshot]; + } + + if (retry == 0 && FBConfiguration.useClearTextShortcut) { + // 1st attempt is via the IOHIDEvent as the fastest operation + // https://github.com/appium/appium/issues/19389 + [[XCUIDevice sharedDevice] fb_performIOHIDEventWithPage:0x07 // kHIDPage_KeyboardOrKeypad + usage:0x9c // kHIDUsage_KeyboardClear + duration:0.01 + error:nil]; + } else if (retry >= MAX_CLEAR_RETRIES - 1) { + // Last chance retry. Tripple-tap the field to select its content + [self tapWithNumberOfTaps:3 numberOfTouches:1]; + return FBTypeText(backspaceDeleteSequence, FBConfiguration.defaultTypingFrequency, error); + } else if (!FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error)) { + // 2nd operation + return NO; + } + + currentValue = [self fb_standardSnapshot].value; + if (nil != placeholderValue && [currentValue isEqualToString:placeholderValue]) { + // Short circuit if only the placeholder value left + return YES; + } + preClearTextLength = [currentValue fb_visualLength]; + + retry++; + } while (preClearTextLength > 0); + return YES; +#else + // tvOS does not need a focus. + // kHIDPage_KeyboardOrKeypad did not work for tvOS's search field. (tvOS 17 at least) + // Tested XCUIElementTypeSearchField and XCUIElementTypeTextView whch were + // common search field and email/passowrd input in tvOS apps. + return FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error); +#endif +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.h b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h new file mode 100644 index 0000000..5a406ad --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBUID) + +/*! Represents unique internal element identifier, which is the same for an element and its snapshot as UUIDv4 */ +@property (nonatomic, nullable, readonly, copy) NSString *fb_uid; + +/*! Represents unique internal element identifier, which is the same for an element and its snapshot */ +@property (nonatomic, readonly) unsigned long long fb_accessibiltyId; + +@end + + +@interface FBXCElementSnapshotWrapper (FBUID) + +/*! Represents unique internal element identifier, which is the same for an element and its snapshot as UUIDv4 */ +@property (nonatomic, nullable, readonly, copy) NSString *fb_uid; + +/*! Represents unique internal element identifier, which is the same for an element and its snapshot */ +@property (nonatomic, readonly) unsigned long long fb_accessibiltyId; + +/** + Fetches wdUID attribute value for the given snapshot instance + + @param snapshot snapshot instance + @return UID attribute value + */ ++ (nullable NSString *)wdUIDWithSnapshot:(id)snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.m b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m new file mode 100644 index 0000000..ebbb066 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "XCUIElement+FBUID.h" + +#import "FBElementUtils.h" +#import "FBLogger.h" +#import "XCUIApplication.h" +#import "XCUIElement+FBUtilities.h" + +@implementation XCUIElement (FBUID) + +- (unsigned long long)fb_accessibiltyId +{ + return [FBElementUtils idWithAccessibilityElement:([self isKindOfClass:XCUIApplication.class] + ? [(XCUIApplication *)self accessibilityElement] + : [self fb_standardSnapshot].accessibilityElement)]; +} + +- (NSString *)fb_uid +{ + return [self isKindOfClass:XCUIApplication.class] + ? [FBElementUtils uidWithAccessibilityElement:[(XCUIApplication *)self accessibilityElement]] + : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_standardSnapshot]].fb_uid; +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBUID) + +static void swizzled_validatePredicateWithExpressionsAllowed(id self, SEL _cmd, id predicate, BOOL withExpressionsAllowed) +{ +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" ++ (void)load +{ + Class XCElementSnapshotCls = objc_lookUpClass("XCElementSnapshot"); + NSAssert(XCElementSnapshotCls != nil, @"Could not locate XCElementSnapshot class"); + Method uidMethod = class_getInstanceMethod(self.class, @selector(fb_uid)); + class_addMethod(XCElementSnapshotCls, @selector(fb_uid), method_getImplementation(uidMethod), method_getTypeEncoding(uidMethod)); + + // Support for Xcode 14.3 requires disabling the new predicate validator, see https://github.com/appium/appium/issues/18444 + Class XCTElementQueryTransformerPredicateValidatorCls = objc_lookUpClass("XCTElementQueryTransformerPredicateValidator"); + if (XCTElementQueryTransformerPredicateValidatorCls == nil) { + return; + } + Method validatePredicateMethod = class_getClassMethod(XCTElementQueryTransformerPredicateValidatorCls, NSSelectorFromString(@"validatePredicate:withExpressionsAllowed:")); + if (validatePredicateMethod == nil) { + [FBLogger log:@"Could not find method +[XCTElementQueryTransformerPredicateValidator validatePredicate:withExpressionsAllowed:]"]; + return; + } + IMP swizzledImp = (IMP)swizzled_validatePredicateWithExpressionsAllowed; + method_setImplementation(validatePredicateMethod, swizzledImp); +} +#pragma diagnostic pop + +- (unsigned long long)fb_accessibiltyId +{ + return [FBElementUtils idWithAccessibilityElement:self.accessibilityElement]; +} + ++ (nullable NSString *)wdUIDWithSnapshot:(id)snapshot +{ + return [FBElementUtils uidWithAccessibilityElement:[snapshot accessibilityElement]]; +} + +- (NSString *)fb_uid +{ + if ([self isKindOfClass:FBXCElementSnapshotWrapper.class]) { + return [self.class wdUIDWithSnapshot:self.snapshot]; + } + return [FBElementUtils uidWithAccessibilityElement:[self accessibilityElement]]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h new file mode 100644 index 0000000..6f46666 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBUtilities) + +/** + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. + Calls to this method mutate the `lastSnapshot` instance property. + The snapshot is taken by the native API provided by XCTest. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Most performant + - Memory-friedly + - `children` property is set to `nil` if not taken from XCUIApplication + - `value` property is cut off to max 512 bytes + + @return The recent snapshot of the element + @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made + */ +- (id)fb_standardSnapshot; + +/** + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. + Calls to this method mutate the `lastSnapshot` instance property. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Less performant in comparison to the standard one + - `children` property is always defined + - `value` property is not cut off + + @return The recent snapshot of the element + @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made + */ +- (id)fb_customSnapshot; + +/** + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. + Calls to this method mutate the `lastSnapshot` instance property. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Less performant in comparison to the standard one + - The `hittable` property calculation is aligned with the native calculation logic + + @return The recent snapshot of the element + @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made + */ +- (id)fb_nativeSnapshot; + +/** + Extracts the cached element snapshot from its query. + No requests to the accessiblity framework is made. + It is only safe to use this call right after element lookup query + has been executed. + + @return Either the cached snapshot or nil + */ +- (nullable id)fb_cachedSnapshot; + +/** + Filters elements by matching them to snapshots from the corresponding array + + @param snapshots Array of snapshots to be matched with + @param onlyChildren Whether to only look for direct element children + + @return Array of filtered elements, which have matches in snapshots array + */ +- (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots + onlyChildren:(BOOL)onlyChildren; + +/** + Waits until element snapshot is stable to avoid "Error copying attributes -25202 error". + This error usually happens for testmanagerd if there is an active UI animation in progress and + causes 15-seconds delay while getting hitpoint value of element's snapshot. +*/ +- (void)fb_waitUntilStable; + +/** + Waits for receiver's snapshot to become stable with the given timeout + + @param timeout The max time to wait util the snapshot is stable +*/ +- (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m new file mode 100644 index 0000000..b627fc8 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBUtilities.h" + +#import + +#import "FBConfiguration.h" +#import "FBExceptions.h" +#import "FBImageUtils.h" +#import "FBElementUtils.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBRunLoopSpinner.h" +#import "FBSettings.h" +#import "FBScreenshot.h" +#import "FBXCAXClientProxy.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshot.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBQuiescence.h" +#import "XCUIApplicationImpl.h" +#import "XCUIApplicationProcess.h" +#import "XCTElementSetTransformer-Protocol.h" +#import "XCTestPrivateSymbols.h" +#import "XCTRunnerDaemonSession.h" +#import "XCUIApplicationProcess+FBQuiescence.h" +#import "XCUIApplication.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElementQuery.h" +#import "XCUIElementQuery+FBHelpers.h" +#import "XCUIElement+FBUID.h" +#import "XCUIScreen.h" +#import "XCUIElement+FBResolve.h" + +@implementation XCUIElement (FBUtilities) + +- (id)fb_takeSnapshot:(BOOL)isCustom +{ + __block id snapshot = nil; + @autoreleasepool { + NSError *error = nil; + snapshot = isCustom + ? [self.fb_query fb_uniqueSnapshotWithError:&error] + : (id)[self snapshotWithError:&error]; + if (nil == snapshot) { + [self fb_raiseStaleElementExceptionWithError:error]; + } + } + self.lastSnapshot = snapshot; + return self.lastSnapshot; +} + +- (id)fb_standardSnapshot +{ + return [self fb_takeSnapshot:NO]; +} + +- (id)fb_customSnapshot +{ + return [self fb_takeSnapshot:YES]; +} + +- (id)fb_nativeSnapshot +{ + NSError *error = nil; + BOOL isSuccessful = [self resolveOrRaiseTestFailure:NO error:&error]; + if (nil == self.lastSnapshot || !isSuccessful) { + [self fb_raiseStaleElementExceptionWithError:error]; + } + return self.lastSnapshot; +} + +- (id)fb_cachedSnapshot +{ + return [self.query fb_cachedSnapshot]; +} + +- (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots + onlyChildren:(BOOL)onlyChildren +{ + if (0 == snapshots.count) { + return @[]; + } + NSMutableArray *matchedIds = [NSMutableArray new]; + for (id snapshot in snapshots) { + @autoreleasepool { + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + [matchedIds addObject:uid]; + } + } + } + NSMutableArray *matchedElements = [NSMutableArray array]; + NSString *uid = nil == self.lastSnapshot + ? self.fb_uid + : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot]; + if (nil != uid && [matchedIds containsObject:uid]) { + XCUIElement *stableSelf = [self fb_stableInstanceWithUid:uid]; + if (1 == snapshots.count) { + return @[stableSelf]; + } + [matchedElements addObject:stableSelf]; + } + XCUIElementType type = XCUIElementTypeAny; + NSArray *uniqueTypes = [snapshots valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfObjects.%@", FBStringify(XCUIElement, elementType)]]; + if (uniqueTypes && [uniqueTypes count] == 1) { + type = [uniqueTypes.firstObject intValue]; + } + XCUIElementQuery *query = onlyChildren + ? [self.fb_query childrenMatchingType:type] + : [self.fb_query descendantsMatchingType:type]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K IN %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), matchedIds]; + [matchedElements addObjectsFromArray:[query matchingPredicate:predicate].allElementsBoundByIndex]; + + for (XCUIElement *el in matchedElements) { + el.fb_isResolvedNatively = @NO; + } + return matchedElements.copy; +} + +- (void)fb_waitUntilStable +{ + [self fb_waitUntilStableWithTimeout:FBConfiguration.waitForIdleTimeout]; +} + +- (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout +{ + if (timeout < DBL_EPSILON) { + return; + } + + NSTimeInterval previousTimeout = FBConfiguration.waitForIdleTimeout; + BOOL previousQuiescence = self.application.fb_shouldWaitForQuiescence; + FBConfiguration.waitForIdleTimeout = timeout; + if (!previousQuiescence) { + self.application.fb_shouldWaitForQuiescence = YES; + } + [[[self.application applicationImpl] currentProcess] + fb_waitForQuiescenceIncludingAnimationsIdle:YES]; + if (previousQuiescence != self.application.fb_shouldWaitForQuiescence) { + self.application.fb_shouldWaitForQuiescence = previousQuiescence; + } + FBConfiguration.waitForIdleTimeout = previousTimeout; +} + +- (void)fb_raiseStaleElementExceptionWithError:(NSError *)error __attribute__((noreturn)) +{ + NSString *hintText = @"Make sure the application UI has the expected state"; + if (nil != error && [error.localizedDescription containsString:@"Identity Binding"]) { + hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText]; + } + NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@", + self.description, hintText]; + if (nil != error) { + reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription]; + } + @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h new file mode 100644 index 0000000..e2355c9 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +@interface FBXCElementSnapshotWrapper (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m new file mode 100644 index 0000000..8ba5b0e --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBVisibleFrame.h" +#import "FBElementUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@implementation XCUIElement (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_visibleFrame; +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + CGRect thisVisibleFrame = [self visibleFrame]; + if (!CGRectIsEmpty(thisVisibleFrame)) { + return thisVisibleFrame; + } + + NSDictionary *visibleFrameDict = [self fb_attributeValue:FB_XCAXAVisibleFrameAttributeName + error:nil]; + if (nil == visibleFrameDict) { + return thisVisibleFrame; + } + + id x = [visibleFrameDict objectForKey:@"X"]; + id y = [visibleFrameDict objectForKey:@"Y"]; + id height = [visibleFrameDict objectForKey:@"Height"]; + id width = [visibleFrameDict objectForKey:@"Width"]; + if (x != nil && y != nil && height != nil && width != nil) { + return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); + } + + return thisVisibleFrame; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h new file mode 100644 index 0000000..8e598d7 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (WebDriverAttributes) + +@end + + +@interface FBXCElementSnapshotWrapper (WebDriverAttributes) + +/** + Fetches wdName attribute value for the given snapshot instance + + @param snapshot snapshot instance + @return wdName attribute value or nil + */ ++ (nullable NSString *)wdNameWithSnapshot:(id)snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m new file mode 100644 index 0000000..ca3023b --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElement+FBWebDriverAttributes.h" + +#import "FBElementTypeTransformer.h" +#import "FBElementHelpers.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBXCElementSnapshotWrapper.h" +#import "XCUIElement+FBAccessibility.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUID.h" +#import "XCUIElement.h" +#import "XCUIElement+FBUtilities.h" +#import "FBElementUtils.h" +#import "XCTestPrivateSymbols.h" +#import "XCUIHitPointResult.h" +#import "FBAccessibilityTraits.h" +#import "XCUIElement+FBMinMax.h" + +#define BROKEN_RECT CGRectMake(-1, -1, 0, 0) + +@implementation XCUIElement (WebDriverAttributesForwarding) + +- (id)fb_snapshotForAttributeName:(NSString *)name +{ + // https://github.com/appium/appium-xcuitest-driver/pull/2565 + if ([name isEqualToString:FBStringify(XCUIElement, isWDHittable)]) { + return [self fb_nativeSnapshot]; + } + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + BOOL isValueRequest = [name isEqualToString:FBStringify(XCUIElement, wdValue)]; + if ([self isKindOfClass:XCUIApplication.class] && !isValueRequest) { + return [self fb_standardSnapshot]; + } + BOOL isCustomSnapshot = [name isEqualToString:FBStringify(XCUIElement, isWDAccessible)] + || [name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)] + || [name isEqualToString:FBStringify(XCUIElement, wdIndex)] + || isValueRequest; + return isCustomSnapshot ? [self fb_customSnapshot] : [self fb_standardSnapshot]; +} + +- (id)fb_valueForWDAttributeName:(NSString *)name +{ + NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:name]; + id snapshot = [self fb_snapshotForAttributeName:wdAttributeName]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_valueForWDAttributeName:name]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + static dispatch_once_t onceToken; + static NSSet *fbElementAttributeNames; + dispatch_once(&onceToken, ^{ + fbElementAttributeNames = [FBElementUtils selectorNamesWithProtocol:@protocol(FBElement)]; + }); + NSString* attributeName = NSStringFromSelector(aSelector); + return [fbElementAttributeNames containsObject:attributeName] + ? [FBXCElementSnapshotWrapper ensureWrapped:[self fb_snapshotForAttributeName:attributeName]] + : nil; +} + +@end + + +@implementation FBXCElementSnapshotWrapper (WebDriverAttributes) + +- (id)fb_valueForWDAttributeName:(NSString *)name +{ + return [self valueForKey:[FBElementUtils wdAttributeNameForAttributeName:name]]; +} + +- (NSNumber *)wdMinValue +{ + return self.fb_minValue; +} + +- (NSNumber *)wdMaxValue +{ + return self.fb_maxValue; +} + +- (NSString *)wdValue +{ + id value = self.value; + XCUIElementType elementType = self.elementType; + if (elementType == XCUIElementTypeStaticText) { + NSString *label = self.label; + value = FBFirstNonEmptyValue(value, label); + } else if (elementType == XCUIElementTypeButton) { + NSNumber *isSelected = self.isSelected ? @YES : nil; + value = FBFirstNonEmptyValue(value, isSelected); + } else if (elementType == XCUIElementTypeSwitch) { + value = @([value boolValue]); + } else if (FBDoesElementSupportInnerText(elementType)) { + NSString *placeholderValue = self.placeholderValue; + value = FBFirstNonEmptyValue(value, placeholderValue); + } + value = FBTransferEmptyStringToNil(value); + if (value) { + value = [NSString stringWithFormat:@"%@", value]; + } + return value; +} + ++ (NSString *)wdNameWithSnapshot:(id)snapshot +{ + NSString *identifier = snapshot.identifier; + if (nil != identifier && identifier.length != 0) { + return identifier; + } + NSString *label = snapshot.label; + return FBTransferEmptyStringToNil(label); +} + +- (NSString *)wdName +{ + return [self.class wdNameWithSnapshot:self.snapshot]; +} + +- (NSString *)wdLabel +{ + XCUIElementType elementType = self.elementType; + return (elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSecureTextField) + ? self.label + : FBTransferEmptyStringToNil(self.label); +} + +- (NSString *)wdPlaceholderValue +{ + return FBDoesElementSupportInnerText(self.elementType) + ? self.placeholderValue + : FBTransferEmptyStringToNil(self.placeholderValue); +} + +- (NSString *)wdType +{ + return [FBElementTypeTransformer stringWithElementType:self.elementType]; +} + +- (NSString *)wdUID +{ + return self.fb_uid; +} + +- (CGRect)wdFrame +{ + CGRect frame = self.frame; + // It is mandatory to replace all Infinity values with numbers to avoid JSON parsing + // exceptions like https://github.com/facebook/WebDriverAgent/issues/639#issuecomment-314421206 + // caused by broken element dimensions returned by XCTest + return (isinf(frame.size.width) || isinf(frame.size.height) + || isinf(frame.origin.x) || isinf(frame.origin.y)) + ? CGRectIntegral(BROKEN_RECT) + : CGRectIntegral(frame); +} + +- (CGRect)wdNativeFrame +{ + // To avoid confusion regarding the frame returned by `wdFrame`, + // the current property is provided to represent the element's + // actual rendered frame. + return self.frame; +} + +/** + Returns a comma-separated string of accessibility traits for the element. + This method converts the element's accessibility traits bitmask into human-readable strings + using FBAccessibilityTraitsToStringsArray. The traits represent various accessibility + characteristics of the element such as Button, Link, Image, etc. + You can find the list of possible traits in the Apple documentation: + https://developer.apple.com/documentation/uikit/uiaccessibilitytraits?language=objc + + @return A comma-separated string of accessibility traits, or an empty string if no traits are set + */ +- (NSString *)wdTraits +{ + NSArray *traits = FBAccessibilityTraitsToStringsArray(self.snapshot.traits); + return [traits componentsJoinedByString:@", "]; +} + +- (BOOL)isWDVisible +{ + return self.fb_isVisible; +} + +- (BOOL)isWDFocused +{ + return self.hasFocus; +} + +- (BOOL)isWDAccessible +{ + XCUIElementType elementType = self.elementType; + // Special cases: + // Table view cell: we consider it accessible if it's container is accessible + // Text fields: actual accessible element isn't text field itself, but nested element + if (elementType == XCUIElementTypeCell) { + if (!self.fb_isAccessibilityElement) { + id containerView = [[self children] firstObject]; + if (![FBXCElementSnapshotWrapper ensureWrapped:containerView].fb_isAccessibilityElement) { + return NO; + } + } + } else if (elementType != XCUIElementTypeTextField && elementType != XCUIElementTypeSecureTextField) { + if (!self.fb_isAccessibilityElement) { + return NO; + } + } + id parentSnapshot = self.parent; + while (parentSnapshot) { + // In the scenario when table provides Search results controller, table could be marked as accessible element, even though it isn't + // As it is highly unlikely that table view should ever be an accessibility element itself, + // for now we work around that by skipping Table View in container checks + if (parentSnapshot.elementType != XCUIElementTypeTable + && [FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement) { + return NO; + } + parentSnapshot = parentSnapshot.parent; + } + return YES; +} + +- (BOOL)isWDAccessibilityContainer +{ + NSArray> *children = self.children; + for (id child in children) { + FBXCElementSnapshotWrapper *wrappedChild = [FBXCElementSnapshotWrapper ensureWrapped:child]; + if (wrappedChild.isWDAccessibilityContainer || wrappedChild.fb_isAccessibilityElement) { + return YES; + } + } + return NO; +} + +- (BOOL)isWDEnabled +{ + return self.isEnabled; +} + +- (BOOL)isWDSelected +{ + return self.isSelected; +} + +- (NSUInteger)wdIndex +{ + if (nil != self.parent) { + for (NSUInteger index = 0; index < self.parent.children.count; ++index) { + if ([self.parent.children objectAtIndex:index] == self.snapshot) { + return index; + } + } + } + + return 0; +} + +- (BOOL)isWDHittable +{ + XCUIHitPointResult *result = [self hitPoint:nil]; + return nil == result ? NO : result.hittable; +} + +- (NSDictionary *)wdRect +{ + CGRect frame = self.wdFrame; + return @{ + @"x": @(CGRectGetMinX(frame)), + @"y": @(CGRectGetMinY(frame)), + @"width": @(CGRectGetWidth(frame)), + @"height": @(CGRectGetHeight(frame)), + }; + } + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h new file mode 100644 index 0000000..8c82890 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "FBXCElementSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElementQuery (FBHelpers) + +/** + Extracts the cached element snapshot from its query. + No requests to the accessiblity framework is made. + It is only safe to use this call right after element lookup query + has been executed. + + @return Either the cached snapshot or nil + */ +- (nullable id)fb_cachedSnapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m new file mode 100644 index 0000000..d2438a0 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElementQuery+FBHelpers.h" + +#import "FBXCodeCompatibility.h" +#import "XCUIElementQuery.h" +#import "FBXCElementSnapshot.h" + +@implementation XCUIElementQuery (FBHelpers) + +- (nullable id)fb_cachedSnapshot +{ + id rootElementSnapshot = self.rootElementSnapshot; + if (nil == rootElementSnapshot) { + return nil; + } + + XCUIElementQuery *inputQuery = self; + NSMutableArray> *transformersChain = [NSMutableArray array]; + while (nil != inputQuery && nil != inputQuery.transformer) { + [transformersChain insertObject:inputQuery.transformer atIndex:0]; + inputQuery = inputQuery.inputQuery; + } + + NSMutableArray *snapshots = [NSMutableArray arrayWithObject:rootElementSnapshot]; + [snapshots addObjectsFromArray:rootElementSnapshot._allDescendants]; + NSOrderedSet *matchingSnapshots = [NSOrderedSet orderedSetWithArray:snapshots]; + @try { + for (id transformer in transformersChain) { + matchingSnapshots = (NSOrderedSet *)[transformer transform:matchingSnapshots + relatedElements:nil]; + } + return matchingSnapshots.count == 1 ? matchingSnapshots.firstObject : nil; + } @catch (NSException *e) { + [FBLogger logFmt:@"Got an unexpected error while retriveing the cached snapshot: %@", e.reason]; + } + return nil; +} + +@end diff --git a/WebDriverAgentLib/Commands/FBAlertViewCommands.h b/WebDriverAgentLib/Commands/FBAlertViewCommands.h new file mode 100644 index 0000000..687c39c --- /dev/null +++ b/WebDriverAgentLib/Commands/FBAlertViewCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBAlertViewCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBAlertViewCommands.m b/WebDriverAgentLib/Commands/FBAlertViewCommands.m new file mode 100644 index 0000000..5047993 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBAlertViewCommands.m @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAlertViewCommands.h" + +#import "FBAlert.h" +#import "FBRouteRequest.h" +#import "FBSession.h" +#import "XCUIApplication+FBHelpers.h" + +@implementation FBAlertViewCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)], + [[FBRoute GET:@"/alert/text"].withoutSession respondWithTarget:self action:@selector(handleAlertGetTextCommand:)], + [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)], + [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)], + [[FBRoute POST:@"/alert/accept"].withoutSession respondWithTarget:self action:@selector(handleAlertAcceptCommand:)], + [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)], + [[FBRoute POST:@"/alert/dismiss"].withoutSession respondWithTarget:self action:@selector(handleAlertDismissCommand:)], + [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)], + ]; +} + + +#pragma mark - Commands + ++ (id)handleAlertGetTextCommand:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + NSString *alertText = [FBAlert alertWithApplication:application].text; + if (!alertText) { + return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil + traceback:nil]); + } + return FBResponseWithObject(alertText); +} + ++ (id)handleAlertSetTextCommand:(FBRouteRequest *)request +{ + FBSession *session = request.session; + id value = request.arguments[@"value"]; + if (!value) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'value' parameter" traceback:nil]); + } + FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication]; + if (!alert.isPresent) { + return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil + traceback:nil]); + } + NSString *textToType = value; + if ([value isKindOfClass:[NSArray class]]) { + textToType = [value componentsJoinedByString:@""]; + } + NSError *error; + if (![alert typeText:textToType error:&error]) { + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return FBResponseWithOK(); +} + ++ (id)handleAlertAcceptCommand:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + NSString *name = request.arguments[@"name"]; + FBAlert *alert = [FBAlert alertWithApplication:application]; + NSError *error; + + if (!alert.isPresent) { + return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil + traceback:nil]); + } + if (name) { + if (![alert clickAlertButton:name error:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + } else if (![alert acceptWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return FBResponseWithOK(); +} + ++ (id)handleAlertDismissCommand:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + NSString *name = request.arguments[@"name"]; + FBAlert *alert = [FBAlert alertWithApplication:application]; + NSError *error; + + if (!alert.isPresent) { + return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil + traceback:nil]); + } + if (name) { + if (![alert clickAlertButton:name error:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + } else if (![alert dismissWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return FBResponseWithOK(); +} + ++ (id)handleGetAlertButtonsCommand:(FBRouteRequest *)request { + FBSession *session = request.session; + FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication]; + + if (!alert.isPresent) { + return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil + traceback:nil]); + } + NSArray *labels = alert.buttonLabels; + return FBResponseWithObject(labels); +} +@end diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.h b/WebDriverAgentLib/Commands/FBCustomCommands.h new file mode 100644 index 0000000..dca1542 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBCustomCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBCustomCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m new file mode 100644 index 0000000..d20c485 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -0,0 +1,633 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBCustomCommands.h" + +#import +#import + +#import "FBConfiguration.h" +#import "FBKeyboard.h" +#import "FBNotificationsHelper.h" +#import "FBMathUtils.h" +#import "FBPasteboard.h" +#import "FBResponsePayload.h" +#import "FBRoute.h" +#import "FBRouteRequest.h" +#import "FBRunLoopSpinner.h" +#import "FBScreen.h" +#import "FBSession.h" +#import "FBXCodeCompatibility.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice+FBHelpers.h" +#import "XCUIElement.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElementQuery.h" +#import "FBUnattachedAppLauncher.h" + +@implementation FBCustomCommands + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)], + [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)], + [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)], + [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)], + [[FBRoute POST:@"/wda/lock"].withoutSession respondWithTarget:self action:@selector(handleLock:)], + [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)], + [[FBRoute POST:@"/wda/unlock"].withoutSession respondWithTarget:self action:@selector(handleUnlock:)], + [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)], + [[FBRoute GET:@"/wda/locked"].withoutSession respondWithTarget:self action:@selector(handleIsLocked:)], + [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)], + [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)], + [[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)], + [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)], + [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)], +#if !TARGET_OS_TV // tvOS does not provide relevant APIs + [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)], + [[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)], + [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)], + [[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)], + [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)], +#endif + [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)], + [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)], + [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)], + [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)], + [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)], + [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)], + [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)], + [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)], + [[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)], + [[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)], + [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)], + [[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)], +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +#if __clang_major__ >= 15 + [[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)], +#endif + [[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)], + [[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)], + [[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)], + [[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)], + [[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], + [[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], +#endif + [[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)], + ]; +} + + +#pragma mark - Commands + ++ (id)handleHomescreenCommand:(FBRouteRequest *)request +{ + NSError *error; + if (![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleDeactivateAppCommand:(FBRouteRequest *)request +{ + NSNumber *requestedDuration = request.arguments[@"duration"]; + NSTimeInterval duration = (requestedDuration ? requestedDuration.doubleValue : 3.); + NSError *error; + if (![request.session.activeApplication fb_deactivateWithDuration:duration error:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id)handleTimeouts:(FBRouteRequest *)request +{ + // This method is intentionally not supported. + return FBResponseWithOK(); +} + ++ (id)handleDismissKeyboardCommand:(FBRouteRequest *)request +{ + NSError *error; + BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"] + error:&error]; + return isDismissed + ? FBResponseWithOK() + : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:nil]); +} + ++ (id)handlePingCommand:(FBRouteRequest *)request +{ + return FBResponseWithOK(); +} + +#pragma mark - Helpers + ++ (id)handleGetScreen:(FBRouteRequest *)request +{ + XCUIApplication *app = XCUIApplication.fb_systemApplication; + + XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject; + CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size; + +#if TARGET_OS_TV + CGSize screenSize = app.frame.size; +#else + CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation); +#endif + + return FBResponseWithObject( + @{ + @"screenSize":@{@"width": @(screenSize.width), + @"height": @(screenSize.height) + }, + @"statusBarSize": @{@"width": @(statusBarSize.width), + @"height": @(statusBarSize.height), + }, + @"scale": @([FBScreen scale]), + }); +} + ++ (id)handleLock:(FBRouteRequest *)request +{ + NSError *error; + if (![[XCUIDevice sharedDevice] fb_lockScreen:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id)handleIsLocked:(FBRouteRequest *)request +{ + BOOL isLocked = [XCUIDevice sharedDevice].fb_isScreenLocked; + return FBResponseWithObject(isLocked ? @YES : @NO); +} + ++ (id)handleUnlock:(FBRouteRequest *)request +{ + NSError *error; + if (![[XCUIDevice sharedDevice] fb_unlockScreen:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id)handleActiveAppInfo:(FBRouteRequest *)request +{ + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + return FBResponseWithObject(@{ + @"pid": @(app.processID), + @"bundleId": app.bundleID, + @"name": app.identifier, + @"processArguments": [self processArguments:app], + }); +} + +/** + * Returns current active app and its arguments of active session + * + * @return The dictionary of current active bundleId and its process/environment argumens + * + * @example + * + * [self currentActiveApplication] + * //=> { + * // "processArguments" : { + * // "env" : { + * // "HAPPY" : "testing" + * // }, + * // "args" : [ + * // "happy", + * // "tseting" + * // ] + * // } + * + * [self currentActiveApplication] + * //=> {} + */ ++ (NSDictionary *)processArguments:(XCUIApplication *)app +{ + // Can be nil if no active activation is defined by XCTest + if (app == nil) { + return @{}; + } + + return + @{ + @"args": app.launchArguments, + @"env": app.launchEnvironment + }; +} + +#if !TARGET_OS_TV ++ (id)handleSetPasteboard:(FBRouteRequest *)request +{ + NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext"; + NSData *content = [[NSData alloc] initWithBase64EncodedString:(NSString *)request.arguments[@"content"] + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + if (nil == content) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Cannot decode the pasteboard content from base64" traceback:nil]); + } + NSError *error; + if (![FBPasteboard setData:content forType:contentType error:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id)handleGetPasteboard:(FBRouteRequest *)request +{ + NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext"; + NSError *error; + id result = [FBPasteboard dataForType:contentType error:&error]; + if (nil == result) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithObject([result base64EncodedStringWithOptions:0]); +} + ++ (id)handleGetBatteryInfo:(FBRouteRequest *)request +{ + if (![[UIDevice currentDevice] isBatteryMonitoringEnabled]) { + [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES]; + } + return FBResponseWithObject(@{ + @"level": @([UIDevice currentDevice].batteryLevel), + @"state": @([UIDevice currentDevice].batteryState) + }); +} +#endif + ++ (id)handlePressButtonCommand:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_pressButton:(id)request.arguments[@"name"] + forDuration:(NSNumber *)request.arguments[@"duration"] + error:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id)handleActivateSiri:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_activateSiriVoiceRecognitionWithText:(id)request.arguments[@"text"] error:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + ++ (id )handlePeformIOHIDEvent:(FBRouteRequest *)request +{ + NSNumber *page = request.arguments[@"page"]; + NSNumber *usage = request.arguments[@"usage"]; + NSNumber *duration = request.arguments[@"duration"]; + NSError *error; + if (![XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:page.unsignedIntValue + usage:usage.unsignedIntValue + duration:duration.doubleValue + error:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id )handleLaunchUnattachedApp:(FBRouteRequest *)request +{ + NSString *bundle = (NSString *)request.arguments[@"bundleId"]; + if ([FBUnattachedAppLauncher launchAppWithBundleId:bundle]) { + return FBResponseWithOK(); + } + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:@"LSApplicationWorkspace failed to launch app" traceback:nil]); +} + ++ (id )handleResetAppAuth:(FBRouteRequest *)request +{ + NSNumber *resource = request.arguments[@"resource"]; + if (nil == resource) { + NSString *errMsg = @"The 'resource' argument must be set to a valid resource identifier (numeric value). See https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:nil]); + } + [request.session.activeApplication resetAuthorizationStatusForResource:(XCUIProtectedResource)resource.longLongValue]; + return FBResponseWithOK(); +} + +/** + Returns device location data. + It requires to configure location access permission by manual. + The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization. + 'authorizationStatus' indicates current authorization status. '3' is 'Always'. + https://developer.apple.com/documentation/corelocation/clauthorizationstatus + + Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always + + The return value could be zero even if the permission is set to 'Always' + since the location service needs some time to update the location data. + */ ++ (id)handleGetLocation:(FBRouteRequest *)request +{ +#if TARGET_OS_TV + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"unsupported" + traceback:nil]); +#else + CLLocationManager *locationManager = [[CLLocationManager alloc] init]; + [locationManager setDistanceFilter:kCLHeadingFilterNone]; + // Always return the best acurate location data + [locationManager setDesiredAccuracy:kCLLocationAccuracyBest]; + [locationManager setPausesLocationUpdatesAutomatically:NO]; + [locationManager startUpdatingLocation]; + + CLAuthorizationStatus authStatus; + if ([locationManager respondsToSelector:@selector(authorizationStatus)]) { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class] + instanceMethodSignatureForSelector:@selector(authorizationStatus)]]; + [invocation setSelector:@selector(authorizationStatus)]; + [invocation setTarget:locationManager]; + [invocation invoke]; + [invocation getReturnValue:&authStatus]; + } else { + authStatus = [CLLocationManager authorizationStatus]; + } + + return FBResponseWithObject(@{ + @"authorizationStatus": @(authStatus), + @"latitude": @(locationManager.location.coordinate.latitude), + @"longitude": @(locationManager.location.coordinate.longitude), + @"altitude": @(locationManager.location.altitude), + }); +#endif +} + ++ (id)handleExpectNotification:(FBRouteRequest *)request +{ + NSString *name = request.arguments[@"name"]; + if (nil == name) { + NSString *message = @"Notification name argument must be provided"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]); + } + NSNumber *timeout = request.arguments[@"timeout"] ?: @60; + NSString *type = request.arguments[@"type"] ?: @"plain"; + + XCTWaiterResult result; + if ([type isEqualToString:@"plain"]) { + result = [FBNotificationsHelper waitForNotificationWithName:name timeout:timeout.doubleValue]; + } else if ([type isEqualToString:@"darwin"]) { + result = [FBNotificationsHelper waitForDarwinNotificationWithName:name timeout:timeout.doubleValue]; + } else { + NSString *message = [NSString stringWithFormat:@"Notification type could only be 'plain' or 'darwin'. Got '%@' instead", type]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]); + } + if (result != XCTWaiterResultCompleted) { + NSString *message = [NSString stringWithFormat:@"Did not receive any expected %@ notifications within %@s", + name, timeout]; + return FBResponseWithStatus([FBCommandStatus timeoutErrorWithMessage:message traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleSetDeviceAppearance:(FBRouteRequest *)request +{ + NSString *name = [request.arguments[@"name"] lowercaseString]; + if (nil == name || !([name isEqualToString:@"light"] || [name isEqualToString:@"dark"])) { + NSString *message = @"The appearance name must be either 'light' or 'dark'"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]); + } + + FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"] + ? FBUIInterfaceAppearanceLight + : FBUIInterfaceAppearanceDark; + NSError *error; + if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleGetDeviceInfo:(FBRouteRequest *)request +{ + // Returns locale like ja_EN and zh-Hant_US. The format depends on OS + // Developers should use this locale by default + // https://developer.apple.com/documentation/foundation/nslocale/1414388-autoupdatingcurrentlocale + NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier]; + + NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary: + @{ + @"currentLocale": currentLocale, + @"timeZone": self.timeZone, + @"name": UIDevice.currentDevice.name, + @"model": UIDevice.currentDevice.model, + @"uuid": [UIDevice.currentDevice.identifierForVendor UUIDString] ?: @"unknown", + // https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom?language=objc + @"userInterfaceIdiom": @(UIDevice.currentDevice.userInterfaceIdiom), + @"userInterfaceStyle": self.userInterfaceStyle, +#if TARGET_OS_SIMULATOR + @"isSimulator": @(YES), +#else + @"isSimulator": @(NO), +#endif + }]; + + // https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate + deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState); + + return FBResponseWithObject(deviceInfo); +} + +/** + * @return Current user interface style as a string + */ ++ (NSString *)userInterfaceStyle +{ + + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { + // Only iOS 15+ simulators/devices return correct data while + // the api itself works in iOS 13 and 14 that has style preference. + NSNumber *appearance = [XCUIDevice.sharedDevice fb_getAppearance]; + if (appearance != nil) { + return [self getAppearanceName:appearance]; + } + } + + static id userInterfaceStyle = nil; + static dispatch_once_t styleOnceToken; + dispatch_once(&styleOnceToken, ^{ + if ([UITraitCollection respondsToSelector:NSSelectorFromString(@"currentTraitCollection")]) { + id currentTraitCollection = [UITraitCollection performSelector:NSSelectorFromString(@"currentTraitCollection")]; + if (nil != currentTraitCollection) { + userInterfaceStyle = [currentTraitCollection valueForKey:@"userInterfaceStyle"]; + } + } + }); + + if (nil == userInterfaceStyle) { + return @"unsupported"; + } + + return [self getAppearanceName:userInterfaceStyle]; +} + ++ (NSString *)getAppearanceName:(NSNumber *)appearance +{ + switch ([appearance longLongValue]) { + case FBUIInterfaceAppearanceUnspecified: + return @"automatic"; + case FBUIInterfaceAppearanceLight: + return @"light"; + case FBUIInterfaceAppearanceDark: + return @"dark"; + default: + return @"unknown"; + } +} + +/** + * @return The string of TimeZone. Returns TZ timezone id by default. Returns TimeZone name by Apple if TZ timezone id is not available. + */ ++ (NSString *)timeZone +{ + NSTimeZone *localTimeZone = [NSTimeZone localTimeZone]; + // Apple timezone name like "US/New_York" + NSString *timeZoneAbb = [localTimeZone abbreviation]; + if (timeZoneAbb == nil) { + return [localTimeZone name]; + } + + // Convert timezone name to ids like "America/New_York" as TZ database Time Zones format + // https://developer.apple.com/documentation/foundation/nstimezone + NSString *timeZoneId = [[NSTimeZone timeZoneWithAbbreviation:timeZoneAbb] name]; + if (timeZoneId != nil) { + return timeZoneId; + } + + return [localTimeZone name]; +} + +#if !TARGET_OS_TV // tvOS does not provide relevant APIs ++ (id)handleGetSimulatedLocation:(FBRouteRequest *)request +{ + NSError *error; + CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + if (nil != error) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(@{ + @"latitude": location ? @(location.coordinate.latitude) : NSNull.null, + @"longitude": location ? @(location.coordinate.longitude) : NSNull.null, + @"altitude": location ? @(location.altitude) : NSNull.null, + }); +} + ++ (id)handleSetSimulatedLocation:(FBRouteRequest *)request +{ + NSNumber *longitude = request.arguments[@"longitude"]; + NSNumber *latitude = request.arguments[@"latitude"]; + + if (nil == longitude || nil == latitude) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided" + traceback:nil]); + } + NSError *error; + CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue + longitude:longitude.doubleValue]; + if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleClearSimulatedLocation:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + +#if __clang_major__ >= 15 ++ (id)handleKeyboardInput:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"]; + XCUIElement *destination = hasElement + ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES] + : request.session.activeApplication; + id keys = request.arguments[@"keys"]; + + if (![destination respondsToSelector:@selector(typeKey:modifierFlags:)]) { + NSString *message = @"typeKey API is only supported since Xcode15 and iPadOS 17"; + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:message + traceback:nil]); + } + + if (![keys isKindOfClass:NSArray.class]) { + NSString *message = @"The 'keys' argument must be an array"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + for (id item in (NSArray *)keys) { + if ([item isKindOfClass:NSString.class]) { + NSString *keyValue = [FBKeyboard keyValueForName:item] ?: item; + [destination typeKey:keyValue modifierFlags:XCUIKeyModifierNone]; + } else if ([item isKindOfClass:NSDictionary.class]) { + id key = [(NSDictionary *)item objectForKey:@"key"]; + if (![key isKindOfClass:NSString.class]) { + NSString *message = [NSString stringWithFormat:@"All dictionaries of 'keys' array must have the 'key' item of type string. Got '%@' instead in the item %@", key, item]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + id modifiers = [(NSDictionary *)item objectForKey:@"modifierFlags"]; + NSUInteger modifierFlags = XCUIKeyModifierNone; + if ([modifiers isKindOfClass:NSNumber.class]) { + modifierFlags = [(NSNumber *)modifiers unsignedIntValue]; + } + NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key; + [destination typeKey:keyValue modifierFlags:modifierFlags]; + } else { + NSString *message = @"All items of the 'keys' array must be either dictionaries or strings"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + } + return FBResponseWithOK(); +} +#endif +#endif + ++ (id)handlePerformAccessibilityAudit:(FBRouteRequest *)request +{ + NSError *error; + NSArray *requestedTypes = request.arguments[@"auditTypes"]; + NSMutableSet *typesSet = [NSMutableSet set]; + if (nil == requestedTypes || 0 == [requestedTypes count]) { + [typesSet addObject:@"XCUIAccessibilityAuditTypeAll"]; + } else { + [typesSet addObjectsFromArray:requestedTypes]; + } + NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy + error:&error]; + if (nil == result) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(result); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBDebugCommands.h b/WebDriverAgentLib/Commands/FBDebugCommands.h new file mode 100644 index 0000000..99728e0 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBDebugCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBDebugCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBDebugCommands.m b/WebDriverAgentLib/Commands/FBDebugCommands.m new file mode 100644 index 0000000..42ea74b --- /dev/null +++ b/WebDriverAgentLib/Commands/FBDebugCommands.m @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBDebugCommands.h" + +#import "FBRouteRequest.h" +#import "FBSession.h" +#import "FBXMLGenerationOptions.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement+FBUtilities.h" +#import "FBXPath.h" + +@implementation FBDebugCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)], + [[FBRoute GET:@"/source"].withoutSession respondWithTarget:self action:@selector(handleGetSourceCommand:)], + [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)], + [[FBRoute GET:@"/wda/accessibleSource"].withoutSession respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)], + ]; +} + + +#pragma mark - Commands + +static NSString *const SOURCE_FORMAT_XML = @"xml"; +static NSString *const SOURCE_FORMAT_JSON = @"json"; +static NSString *const SOURCE_FORMAT_DESCRIPTION = @"description"; + ++ (id)handleGetSourceCommand:(FBRouteRequest *)request +{ + // This method might be called without session + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + NSString *sourceType = request.parameters[@"format"] ?: SOURCE_FORMAT_XML; + NSString *sourceScope = request.parameters[@"scope"]; + id result; + if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_XML] == NSOrderedSame) { + NSArray *excludedAttributes = nil == request.parameters[@"excluded_attributes"] + ? nil + : [request.parameters[@"excluded_attributes"] componentsSeparatedByString:@","]; + result = [application fb_xmlRepresentationWithOptions: + [[[FBXMLGenerationOptions new] + withExcludedAttributes:excludedAttributes] + withScope:sourceScope]]; + } else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_JSON] == NSOrderedSame) { + NSString *excludedAttributesString = request.parameters[@"excluded_attributes"]; + NSSet *excludedAttributes = (excludedAttributesString == nil) + ? nil + : [NSSet setWithArray:[excludedAttributesString componentsSeparatedByString:@","]]; + + result = [application fb_tree:excludedAttributes]; + } else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_DESCRIPTION] == NSOrderedSame) { + result = application.fb_descriptionRepresentation; + } else { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"Unknown source format '%@'. Only %@ source formats are supported.", + sourceType, @[SOURCE_FORMAT_XML, SOURCE_FORMAT_JSON, SOURCE_FORMAT_DESCRIPTION]] traceback:nil]); + } + if (nil == result) { + return FBResponseWithUnknownErrorFormat(@"Cannot get '%@' source of the current application", sourceType); + } + return FBResponseWithObject(result); +} + ++ (id)handleGetAccessibleSourceCommand:(FBRouteRequest *)request +{ + // This method might be called without session + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + return FBResponseWithObject(application.fb_accessibilityTree ?: @{}); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBElementCommands.h b/WebDriverAgentLib/Commands/FBElementCommands.h new file mode 100644 index 0000000..3c07ee6 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBElementCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBElementCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBElementCommands.m b/WebDriverAgentLib/Commands/FBElementCommands.m new file mode 100644 index 0000000..227ca14 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBElementCommands.m @@ -0,0 +1,819 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementCommands.h" + +#import "FBConfiguration.h" +#import "FBKeyboard.h" +#import "FBRoute.h" +#import "FBRouteRequest.h" +#import "FBRunLoopSpinner.h" +#import "FBElementCache.h" +#import "FBErrorBuilder.h" +#import "FBSession.h" +#import "FBElementUtils.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBRuntimeUtils.h" +#import "NSPredicate+FBFormat.h" +#import "XCTestPrivateSymbols.h" +#import "XCUICoordinate.h" +#import "XCUIDevice.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBPickerWheel.h" +#import "XCUIElement+FBScrolling.h" +#import "XCUIElement+FBForceTouch.h" +#import "XCUIElement+FBSwiping.h" +#import "XCUIElement+FBTyping.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElement+FBTVFocuse.h" +#import "XCUIElement+FBResolve.h" +#import "XCUIElement+FBUID.h" +#import "FBElementTypeTransformer.h" +#import "XCUIElement.h" +#import "XCUIElementQuery.h" +#import "FBXCodeCompatibility.h" +// 监听网络 +#import +#import +#import +#import +#import +#import + +@interface FBElementCommands () +@end + +@implementation FBElementCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)], + [[FBRoute GET:@"/window/rect"] respondWithTarget:self action:@selector(handleGetWindowRect:)], + [[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)], + [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)], + [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)], + [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)], + [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)], + [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)], + [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)], + [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)], + [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)], + [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)], + [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)], + // W3C element screenshot + [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)], + // JSONWP element screenshot + [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)], + [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)], + [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)], +#if TARGET_OS_TV + [[FBRoute GET:@"/element/:uuid/attribute/focused"] respondWithTarget:self action:@selector(handleGetFocused:)], + [[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)], +#else + [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)], + [[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)], + + [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)], + [[FBRoute POST:@"/wda/pinch"] respondWithTarget:self action:@selector(handlePinch:)], + + [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)], + [[FBRoute POST:@"/wda/rotate"] respondWithTarget:self action:@selector(handleRotate:)], + + [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)], + [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)], + + [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)], + [[FBRoute POST:@"/wda/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)], + + [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self + action:@selector(handleTapWithNumberOfTaps:)], + [[FBRoute POST:@"/wda/tapWithNumberOfTaps"] respondWithTarget:self + action:@selector(handleTapWithNumberOfTaps:)], + + [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)], + [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)], + + [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)], + [[FBRoute POST:@"/wda/scroll"] respondWithTarget:self action:@selector(handleScroll:)], + + [[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)], + + [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)], + [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)], + + [[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)], + [[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)], + + [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], + [[FBRoute POST:@"/wda/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], + + [[FBRoute POST:@"/wda/element/:uuid/tap"] respondWithTarget:self action:@selector(handleTap:)], + [[FBRoute POST:@"/wda/tap"] respondWithTarget:self action:@selector(handleTap:)], + + //添加网络监听方法 张伟 临时添加 + [[FBRoute GET:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)], + [[FBRoute POST:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)], + + [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)], +#endif + [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] + ]; +} + + +#pragma mark - Commands +// 网络监听回调 ++ (id)handleNetWorkStatus:(FBRouteRequest *)request +{ + BOOL reachable = FBHasExternalConnectivityViaHTTPS(); + return FBResponseWithObject(@(reachable)); +} + +// 检测网络(更稳:更长超时 + 正确处理 wait 超时 + 更清晰的日志) +static BOOL FBHasExternalConnectivityViaHTTPS(void) { + __block BOOL ok = NO; + + // 仍然保留你的 TikTok 域名探测 + NSArray *urlStrings = @[ + @"https://www.tiktok.com/robots.txt", + @"https://www.tiktok.com/", + @"https://m.tiktok.com/", + @"https://www.tiktokv.com/", + @"https://api.tiktokv.com/" + ]; + + // ✅ 改:用 default 配置(更接近系统正常网络栈),并把超时拉长 + NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; + cfg.timeoutIntervalForRequest = 12.0; + cfg.timeoutIntervalForResource = 12.0; + + if (@available(iOS 11.0, *)) { + // ✅ 改:网络刚切换/刚连上时更稳,不会立刻失败 + cfg.waitsForConnectivity = YES; + } + + NSURLSession *s = [NSURLSession sessionWithConfiguration:cfg]; + + // ✅ 改:单个 URL 最多等 12 秒(和 timeoutIntervalForRequest 对齐) + const NSTimeInterval perURLWaitSeconds = 12.0; + + for (NSString *urlStr in urlStrings) { + if (ok) { break; } + + NSURL *url = [NSURL URLWithString:urlStr]; + if (!url) { + NSLog(@"[NetCheck] invalid url: %@", urlStr); + continue; + }å + + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + + // ✅ 改:用 request,便于设置 UA/缓存策略/超时 + NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url]; + req.HTTPMethod = @"GET"; + req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + req.timeoutInterval = perURLWaitSeconds; + + // ✅ 可选但推荐:给一个普通 UA,避免被某些 WAF 当成“脚本默认 UA”更严格对待 + [req setValue:@"Mozilla/5.0 (iPhone; CPU iPhone OS like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile" +forHTTPHeaderField:@"User-Agent"]; + + __block NSString *localErrDomain = nil; + __block NSInteger localErrCode = 0; + __block NSInteger localHttpCode = -1; + + [[s dataTaskWithRequest:req completionHandler:^(NSData *d, NSURLResponse *r, NSError *e) { + if (e) { + localErrDomain = e.domain ?: @""; + localErrCode = e.code; + NSLog(@"[NetCheck] error (%@): domain=%@ code=%ld desc=%@ userInfo=%@", + urlStr, localErrDomain, (long)localErrCode, e.localizedDescription, e.userInfo); + } else { + if ([r isKindOfClass:NSHTTPURLResponse.class]) { + localHttpCode = ((NSHTTPURLResponse *)r).statusCode; + NSLog(@"[NetCheck] HTTP (%@) = %ld", urlStr, (long)localHttpCode); + + // ✅ 改:只要拿到 HTTP 响应(哪怕 301/403/404),说明“能连到站点” + // 你原来是“无网络层错误就 ok”,这里更显式 + ok = YES; + } else { + NSLog(@"[NetCheck] response (%@): %@", urlStr, r); + ok = YES; + } + } + dispatch_semaphore_signal(sem); + }] resume]; + + long waitResult = dispatch_semaphore_wait( + sem, + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(perURLWaitSeconds * NSEC_PER_SEC)) + ); + + // ✅ 改:如果等超时,明确记录一次(这在旧机上非常关键) + if (waitResult != 0) { + NSLog(@"[NetCheck] wait timeout (%@) after %.0fs (http=%ld err=%@/%ld)", + urlStr, perURLWaitSeconds, (long)localHttpCode, + localErrDomain ?: @"", (long)localErrCode); + // 这里不把 ok 置为 NO(本来就是 NO),继续试下一个域名 + } + + // 如果已经 ok,就提前结束 + if (ok) { break; } + } + + [s finishTasksAndInvalidate]; + NSLog(@"[NetCheck] TikTok reachability via HTTPS: %@", ok ? @"YES" : @"NO"); + return ok; +} + ++ (id)handleGetEnabled:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDEnabled)); +} + ++ (id)handleGetRect:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(element.wdRect); +} + ++ (id)handleGetAttribute:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + NSString *attributeName = request.parameters[@"name"]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id attributeValue = [element fb_valueForWDAttributeName:attributeName]; + return FBResponseWithObject(attributeValue ?: [NSNull null]); +} + ++ (id)handleGetText:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + id snapshot = [element fb_customSnapshot]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel); + return FBResponseWithObject(text ?: @""); +} + ++ (id)handleGetDisplayed:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDVisible)); +} + ++ (id)handleGetAccessible:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessible)); +} + ++ (id)handleGetIsAccessibilityContainer:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessibilityContainer)); +} + ++ (id)handleGetName:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(element.wdType); +} + ++ (id)handleGetSelected:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.wdSelected)); +} + ++ (id)handleSetValue:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + id value = request.arguments[@"value"] ?: request.arguments[@"text"]; + if (!value) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]); + } + NSString *textToType = [value isKindOfClass:NSArray.class] + ? [value componentsJoinedByString:@""] + : value; + XCUIElementType elementType = [element elementType]; +#if !TARGET_OS_TV + if (elementType == XCUIElementTypePickerWheel) { + [element adjustToPickerWheelValue:textToType]; + return FBResponseWithOK(); + } +#endif + if (elementType == XCUIElementTypeSlider) { + CGFloat sliderValue = textToType.floatValue; + if (sliderValue < 0.0 || sliderValue > 1.0 ) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Value of slider should be in 0..1 range" traceback:nil]); + } + [element adjustToNormalizedSliderPosition:sliderValue]; + return FBResponseWithOK(); + } + NSUInteger frequency = (NSUInteger)[request.arguments[@"frequency"] longLongValue] ?: [FBConfiguration maxTypingFrequency]; + NSError *error = nil; + if (![element fb_typeText:textToType + shouldClear:NO + frequency:frequency + error:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleClick:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES]; +#if TARGET_OS_IOS + [element tap]; +#elif TARGET_OS_TV + NSError *error = nil; + if (![element fb_selectWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } +#endif + return FBResponseWithOK(); +} + ++ (id)handleClear:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + NSError *error; + if (![element fb_clearTextWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } + return FBResponseWithOK(); +} + +#if TARGET_OS_TV ++ (id)handleGetFocused:(FBRouteRequest *)request +{ + // `BOOL isFocused = [elementCache elementForUUID:request.parameters[@"uuid"]];` + // returns wrong true/false after moving focus by key up/down, for example. + // Thus, ensure the focus compares the status with `fb_focusedElement`. + BOOL isFocused = NO; + XCUIElement *focusedElement = request.session.activeApplication.fb_focusedElement; + if (focusedElement != nil) { + FBElementCache *elementCache = request.session.elementCache; + BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy; + NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy + ? focusedElement + : [focusedElement fb_stableInstanceWithUid:focusedElement.fb_uid])]; + focusedElement.lastSnapshot = nil; + if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) { + isFocused = YES; + } + } + + return FBResponseWithObject(@(isFocused)); +} + ++ (id)handleFocuse:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + NSError *error; + if (![element fb_setFocusWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } + return FBResponseWithStatus([FBCommandStatus okWithValue: FBDictionaryResponseWithElement(element, FBConfiguration.shouldUseCompactResponses)]); +} +#else ++ (id)handleDoubleTap:(FBRouteRequest *)request +{ + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target doubleTap]; + return FBResponseWithOK(); +} + ++ (id)handleTwoFingerTap:(FBRouteRequest *)request +{ + XCUIElement *element = [self targetFromRequest:request]; + [element twoFingerTap]; + return FBResponseWithOK(); +} + ++ (id)handleTapWithNumberOfTaps:(FBRouteRequest *)request +{ + if (nil == request.arguments[@"numberOfTaps"] || nil == request.arguments[@"numberOfTouches"]) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'numberOfTaps' and 'numberOfTouches' arguments must be provided" + traceback:nil]); + } + XCUIElement *element = [self targetFromRequest:request]; + [element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue] + numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]]; + return FBResponseWithOK(); +} + ++ (id)handleTouchAndHold:(FBRouteRequest *)request +{ + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target pressForDuration:[request.arguments[@"duration"] doubleValue]]; + return FBResponseWithOK(); +} + ++ (id)handlePressAndDragWithVelocity:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [self targetFromRequest:request]; + [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue] + thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"] checkStaleness:YES] + withVelocity:[request.arguments[@"velocity"] doubleValue] + thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; + return FBResponseWithOK(); +} + ++ (id)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request +{ + FBSession *session = request.session; + CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue], + (CGFloat)[request.arguments[@"fromY"] doubleValue]); + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset + element:session.activeApplication]; + CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue], + (CGFloat)[request.arguments[@"toY"] doubleValue]); + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset + element:session.activeApplication]; + [startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue] + thenDragToCoordinate:endCoordinate + withVelocity:[request.arguments[@"velocity"] doubleValue] + thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; + return FBResponseWithOK(); +} + ++ (id)handleScroll:(FBRouteRequest *)request +{ + XCUIElement *element = [self targetFromRequest:request]; + // Using presence of arguments as a way to convey control flow seems like a pretty bad idea but it's + // what ios-driver did and sadly, we must copy them. + NSString *const name = request.arguments[@"name"]; + if (name) { + XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:name] allElementsBoundByIndex] lastObject]; + if (!childElement) { + return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name] + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return [self.class handleScrollElementToVisible:childElement withRequest:request]; + } + + NSString *const direction = request.arguments[@"direction"]; + if (direction) { + NSString *const distanceString = request.arguments[@"distance"] ?: @"1.0"; + CGFloat distance = (CGFloat)distanceString.doubleValue; + if ([direction isEqualToString:@"up"]) { + [element fb_scrollUpByNormalizedDistance:distance]; + } else if ([direction isEqualToString:@"down"]) { + [element fb_scrollDownByNormalizedDistance:distance]; + } else if ([direction isEqualToString:@"left"]) { + [element fb_scrollLeftByNormalizedDistance:distance]; + } else if ([direction isEqualToString:@"right"]) { + [element fb_scrollRightByNormalizedDistance:distance]; + } + return FBResponseWithOK(); + } + + NSString *const predicateString = request.arguments[@"predicateString"]; + if (predicateString) { + NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate + predicateWithFormat:predicateString]]; + XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:formattedPredicate] allElementsBoundByIndex] lastObject]; + if (!childElement) { + return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString] + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return [self.class handleScrollElementToVisible:childElement withRequest:request]; + } + + if (request.arguments[@"toVisible"]) { + return [self.class handleScrollElementToVisible:element withRequest:request]; + } + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Unsupported scroll type" traceback:nil]); +} + ++ (id)handleScrollTo:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + NSError *error; + return [element fb_nativeScrollToVisibleWithError:&error] + ? FBResponseWithOK() + : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:nil]); +} + ++ (id)handleDrag:(FBRouteRequest *)request +{ + XCUIElement *target = [self targetFromRequest:request]; + CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], + [request.arguments[@"fromY"] doubleValue]); + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target]; + CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue], + [request.arguments[@"toY"] doubleValue]); + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target]; + NSTimeInterval duration = [request.arguments[@"duration"] doubleValue]; + [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; + return FBResponseWithOK(); +} + ++ (id)handleSwipe:(FBRouteRequest *)request +{ + NSString *const direction = request.arguments[@"direction"]; + if (!direction) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]); + } + NSArray *supportedDirections = @[@"up", @"down", @"left", @"right"]; + if (![supportedDirections containsObject:direction.lowercaseString]) { + NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target fb_swipeWithDirection:direction velocity:request.arguments[@"velocity"]]; + return FBResponseWithOK(); +} + ++ (id)handleTap:(FBRouteRequest *)request +{ + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target tap]; + return FBResponseWithOK(); +} + ++ (id)handlePinch:(FBRouteRequest *)request +{ + XCUIElement *element = [self targetFromRequest:request]; + CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue]; + CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue]; + [element pinchWithScale:scale velocity:velocity]; + return FBResponseWithOK(); +} + ++ (id)handleRotate:(FBRouteRequest *)request +{ + XCUIElement *element = [self targetFromRequest:request]; + CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue]; + CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue]; + [element rotate:rotation withVelocity:velocity]; + return FBResponseWithOK(); +} + ++ (id)handleForceTouch:(FBRouteRequest *)request +{ + XCUIElement *element = [self targetFromRequest:request]; + NSNumber *pressure = request.arguments[@"pressure"]; + NSNumber *duration = request.arguments[@"duration"]; + NSNumber *x = request.arguments[@"x"]; + NSNumber *y = request.arguments[@"y"]; + NSValue *hitPoint = (nil == x || nil == y) + ? nil + : [NSValue valueWithCGPoint:CGPointMake((CGFloat)[x doubleValue], (CGFloat)[y doubleValue])]; + NSError *error; + BOOL didSucceed = [element fb_forceTouchCoordinate:hitPoint + pressure:pressure + duration:duration + error:&error]; + return didSucceed + ? FBResponseWithOK() + : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:nil]); +} +#endif + ++ (id)handleKeys:(FBRouteRequest *)request +{ + NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""]; + NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency]; + NSError *error; + if (!FBTypeText(textToType, frequency, &error)) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleGetWindowSize:(FBRouteRequest *)request +{ + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + + CGRect frame = app.wdFrame; +#if TARGET_OS_TV + CGSize screenSize = frame.size; +#else + CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); +#endif + return FBResponseWithObject(@{ + @"width": @(screenSize.width), + @"height": @(screenSize.height), + }); +} + + ++ (id)handleGetWindowRect:(FBRouteRequest *)request +{ + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + + CGRect frame = app.wdFrame; +#if TARGET_OS_TV + CGSize screenSize = frame.size; +#else + CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); +#endif + return FBResponseWithObject(@{ + @"x": @(frame.origin.x), + @"y": @(frame.origin.y), + @"width": @(screenSize.width), + @"height": @(screenSize.height), + }); +} + ++ (id)handleElementScreenshot:(FBRouteRequest *)request +{ + @autoreleasepool { + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + NSData *screenshotData = nil; + @autoreleasepool { + screenshotData = [element.screenshot PNGRepresentation]; + if (nil == screenshotData) { + NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; + return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg + traceback:nil]); + } + } + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + screenshotData = nil; + return FBResponseWithObject(screenshot); + } +} + + +#if !TARGET_OS_TV +static const CGFloat DEFAULT_PICKER_OFFSET = (CGFloat)0.2; +static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25; + + ++ (id)handleWheelSelect:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + if ([element elementType] != XCUIElementTypePickerWheel) { + NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + NSString* order = [request.arguments[@"order"] lowercaseString]; + CGFloat offset = DEFAULT_PICKER_OFFSET; + if (request.arguments[@"offset"]) { + offset = (CGFloat)[request.arguments[@"offset"] doubleValue]; + if (offset <= 0.0 || offset > 0.5) { + NSString *errMsg = [NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + } + NSNumber *maxAttempts = request.arguments[@"maxAttempts"] ?: @(DEFAULT_MAX_PICKER_ATTEMPTS); + NSString *expectedValue = request.arguments[@"value"]; + NSInteger attempt = 0; + while (attempt < [maxAttempts integerValue]) { + BOOL isSuccessful = false; + NSError *error; + if ([order isEqualToString:@"next"]) { + isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error]; + } else if ([order isEqualToString:@"previous"]) { + isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error]; + } else { + NSString *errMsg = [NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + if (!isSuccessful) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } + if (nil == expectedValue || [element.wdValue isEqualToString:expectedValue]) { + return FBResponseWithOK(); + } + attempt++; + } + NSString *errMsg = [NSString stringWithFormat:@"Cannot select the expected picker wheel value '%@' after %ld attempts", expectedValue, attempt]; + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMsg traceback:nil]); +} + +#pragma mark - Helpers + ++ (id)handleScrollElementToVisible:(XCUIElement *)element withRequest:(FBRouteRequest *)request +{ + NSError *error; + if (!element.exists) { + return FBResponseWithStatus([FBCommandStatus elementNotVisibleErrorWithMessage:@"Can't scroll to element that does not exist" traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + if (![element fb_scrollToVisibleWithError:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + return FBResponseWithOK(); +} + +/** + Returns gesture coordinate for the element based on absolute coordinate + + @param offset absolute screen offset for the given application + @param element the element instance to perform the gesture on + @return translated gesture coordinates ready to be passed to XCUICoordinate methods + */ ++ (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset + element:(XCUIElement *)element +{ + return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; +} + +/** + Returns either coordinates or the target element for the given request that expects 'x' and 'y' coordannates + + @param request HTTP request object + @param error Error instance if any + @return Either XCUICoordinate or XCUIElement instance. nil if the input data is invalid + */ ++ (nullable id)targetWithXyCoordinatesFromRequest:(FBRouteRequest *)request error:(NSError **)error +{ + NSNumber *x = request.arguments[@"x"]; + NSNumber *y = request.arguments[@"y"]; + if (nil == x && nil == y) { + return [self targetFromRequest:request]; + } + if ((nil == x && nil != y) || (nil != x && nil == y)) { + [[[FBErrorBuilder alloc] + withDescription:@"Both x and y coordinates must be provided"] + buildError:error]; + return nil; + } + return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue) + element:[self targetFromRequest:request]]; +} + +/** + Returns the target element for the given request + + @param request HTTP request object + @return Matching XCUIElement instance + */ ++ (XCUIElement *)targetFromRequest:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + NSString *elementUuid = (NSString *)request.parameters[@"uuid"]; + return nil == elementUuid + ? request.session.activeApplication + : [elementCache elementForUUID:elementUuid checkStaleness:YES]; +} + +#endif + +@end diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.h b/WebDriverAgentLib/Commands/FBFindElementCommands.h new file mode 100644 index 0000000..7349e10 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBFindElementCommands : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.m b/WebDriverAgentLib/Commands/FBFindElementCommands.m new file mode 100644 index 0000000..c370f11 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.m @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBFindElementCommands.h" + +#import "FBAlert.h" +#import "FBConfiguration.h" +#import "FBElementCache.h" +#import "FBExceptions.h" +#import "FBMacros.h" +#import "FBRouteRequest.h" +#import "FBSession.h" +#import "XCTestPrivateSymbols.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement+FBClassChain.h" +#import "XCUIElement+FBFind.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" + +static id FBNoSuchElementErrorResponseForRequest(FBRouteRequest *request) +{ + return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"unable to find an element using '%@', value '%@'", request.arguments[@"using"], request.arguments[@"value"]] + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); +} + +@implementation FBFindElementCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)], + [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)], + [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)], + [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)], + [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)], +#if TARGET_OS_TV + [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetFocusedElement:)], +#else + [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)], +#endif + ]; +} + + +#pragma mark - Commands + ++ (id)handleFindElement:(FBRouteRequest *)request +{ + FBSession *session = request.session; + XCUIElement *element = [self.class elementUsing:request.arguments[@"using"] + withValue:request.arguments[@"value"] + under:session.activeApplication]; + if (!element) { + return FBNoSuchElementErrorResponseForRequest(request); + } + return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + ++ (id)handleFindElements:(FBRouteRequest *)request +{ + FBSession *session = request.session; + NSArray *elements = [self.class elementsUsing:request.arguments[@"using"] + withValue:request.arguments[@"value"] + under:session.activeApplication + shouldReturnAfterFirstMatch:NO]; + return FBResponseWithCachedElements(elements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + ++ (id)handleFindVisibleCells:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id snapshot = [element fb_customSnapshot]; + NSArray> *visibleCellSnapshots = [snapshot descendantsByFilteringWithBlock:^BOOL(id shot) { + return shot.elementType == XCUIElementTypeCell + && [FBXCElementSnapshotWrapper ensureWrapped:shot].wdVisible; + }]; + NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots + onlyChildren:NO]; + return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + ++ (id)handleFindSubElement:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; + XCUIElement *foundElement = [self.class elementUsing:request.arguments[@"using"] + withValue:request.arguments[@"value"] + under:element]; + if (!foundElement) { + return FBNoSuchElementErrorResponseForRequest(request); + } + return FBResponseWithCachedElement(foundElement, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + ++ (id)handleFindSubElements:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; + NSArray *foundElements = [self.class elementsUsing:request.arguments[@"using"] + withValue:request.arguments[@"value"] + under:element + shouldReturnAfterFirstMatch:NO]; + return FBResponseWithCachedElements(foundElements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + ++ (id)handleGetActiveElement:(FBRouteRequest *)request +{ + XCUIElement *element = request.session.activeApplication.fb_activeElement; + if (nil == element) { + return FBNoSuchElementErrorResponseForRequest(request); + } + return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} + +#if TARGET_OS_TV ++ (id)handleGetFocusedElement:(FBRouteRequest *)request +{ + XCUIElement *element = request.session.activeApplication.fb_focusedElement; + return element == nil + ? FBNoSuchElementErrorResponseForRequest(request) + : FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); +} +#endif + +#pragma mark - Helpers + ++ (XCUIElement *)elementUsing:(NSString *)usingText withValue:(NSString *)value under:(XCUIElement *)element +{ + return [[self elementsUsing:usingText + withValue:value + under:element + shouldReturnAfterFirstMatch:YES] firstObject]; +} + ++ (NSArray *)elementsUsing:(NSString *)usingText + withValue:(NSString *)value + under:(XCUIElement *)element +shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch +{ + if ([usingText isEqualToString:@"partial link text"] + || [usingText isEqualToString:@"link text"]) { + NSArray *components = [value componentsSeparatedByString:@"="]; + NSString *propertyValue = components.lastObject; + NSString *propertyName = (components.count < 2 ? @"name" : components.firstObject); + return [element fb_descendantsMatchingProperty:propertyName + value:propertyValue + partialSearch:[usingText containsString:@"partial"]]; + } else if ([usingText isEqualToString:@"class name"]) { + return [element fb_descendantsMatchingClassName:value + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; + } else if ([usingText isEqualToString:@"class chain"]) { + return [element fb_descendantsMatchingClassChain:value + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; + } else if ([usingText isEqualToString:@"xpath"]) { + return [element fb_descendantsMatchingXPathQuery:value + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; + } else if ([usingText isEqualToString:@"predicate string"]) { + return [element fb_descendantsMatchingPredicate:[NSPredicate predicateWithFormat:value] + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; + } else if ([usingText isEqualToString:@"name"] + || [usingText isEqualToString:@"id"] + || [usingText isEqualToString:@"accessibility id"]) { + return [element fb_descendantsMatchingIdentifier:value + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; + } else { + @throw [NSException exceptionWithName:FBElementAttributeUnknownException + reason:[NSString stringWithFormat:@"Invalid locator requested: %@", usingText] + userInfo:nil]; + } +} + +@end + diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.h b/WebDriverAgentLib/Commands/FBOrientationCommands.h new file mode 100644 index 0000000..1aaaacd --- /dev/null +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBOrientationCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.m b/WebDriverAgentLib/Commands/FBOrientationCommands.m new file mode 100644 index 0000000..8e0bea4 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.m @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBOrientationCommands.h" +#import "XCUIDevice+FBRotation.h" +#import "FBRouteRequest.h" +#import "FBMacros.h" +#import "FBSession.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice.h" + +extern const struct FBWDOrientationValues { + FBLiteralString portrait; + FBLiteralString landscapeLeft; + FBLiteralString landscapeRight; + FBLiteralString portraitUpsideDown; +} FBWDOrientationValues; + +const struct FBWDOrientationValues FBWDOrientationValues = { + .portrait = @"PORTRAIT", + .landscapeLeft = @"LANDSCAPE", + .landscapeRight = @"UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT", + .portraitUpsideDown = @"UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN", +}; + +#if !TARGET_OS_TV + +@implementation FBOrientationCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)], + [[FBRoute GET:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleGetOrientation:)], + [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)], + [[FBRoute POST:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleSetOrientation:)], + [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)], + [[FBRoute GET:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleGetRotation:)], + [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)], + [[FBRoute POST:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleSetRotation:)], + ]; +} + + +#pragma mark - Commands + ++ (id)handleGetOrientation:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + NSString *orientation = [self.class interfaceOrientationForApplication:application]; + return FBResponseWithObject([[self _wdOrientationsMapping] objectForKey:orientation]); +} + ++ (id)handleSetOrientation:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + if ([self.class setDeviceOrientation:request.arguments[@"orientation"] forApplication:application]) { + return FBResponseWithOK(); + } + + return FBResponseWithUnknownErrorFormat(@"Unable To Rotate Device"); +} + ++ (id)handleGetRotation:(FBRouteRequest *)request +{ + XCUIDevice *device = [XCUIDevice sharedDevice]; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + UIInterfaceOrientation orientation = application.interfaceOrientation; + return FBResponseWithObject(device.fb_rotationMapping[@(orientation)]); +} + ++ (id)handleSetRotation:(FBRouteRequest *)request +{ + if (nil == request.arguments[@"x"] || nil == request.arguments[@"y"] || nil == request.arguments[@"z"]) { + NSString *errMessage = [NSString stringWithFormat:@"x, y and z arguments must exist in the request body: %@", request.arguments]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage + traceback:nil]); + } + + NSDictionary* rotation = @{ + @"x": request.arguments[@"x"] ?: @0, + @"y": request.arguments[@"y"] ?: @0, + @"z": request.arguments[@"z"] ?: @0, + }; + NSArray *supportedRotations = XCUIDevice.sharedDevice.fb_rotationMapping.allValues; + if (![supportedRotations containsObject:rotation]) { + NSString *errMessage = [ + NSString stringWithFormat:@"%@ rotation is not supported. Only the following values are supported: %@", + rotation, supportedRotations + ]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage + traceback:nil]); + } + + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + if (![self.class setDeviceRotation:request.arguments forApplication:application]) { + NSString *errMessage = [ + NSString stringWithFormat:@"The current rotation cannot be set to %@. Make sure the %@ application supports it", + rotation, application.bundleID + ]; + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMessage + traceback:nil]); + } + return FBResponseWithOK(); +} + + +#pragma mark - Helpers + ++ (NSString *)interfaceOrientationForApplication:(XCUIApplication *)application +{ + NSNumber *orientation = @(application.interfaceOrientation); + NSSet *keys = [[self _orientationsMapping] keysOfEntriesPassingTest:^BOOL(id key, NSNumber *obj, BOOL *stop) { + return [obj isEqualToNumber:orientation]; + }]; + if (keys.count == 0) { + return @"Unknown orientation"; + } + return keys.anyObject; +} + ++ (BOOL)setDeviceRotation:(NSDictionary *)rotationObj forApplication:(XCUIApplication *)application +{ + return [[XCUIDevice sharedDevice] fb_setDeviceRotation:rotationObj]; +} + ++ (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(XCUIApplication *)application +{ + NSNumber *orientationValue = [[self _orientationsMapping] objectForKey:[orientation uppercaseString]]; + if (orientationValue == nil) { + return NO; + } + return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientationValue.integerValue]; +} + ++ (NSDictionary *)_orientationsMapping +{ + static NSDictionary *orientationMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + orientationMap = + @{ + FBWDOrientationValues.portrait : @(UIDeviceOrientationPortrait), + FBWDOrientationValues.portraitUpsideDown : @(UIDeviceOrientationPortraitUpsideDown), + FBWDOrientationValues.landscapeLeft : @(UIDeviceOrientationLandscapeLeft), + FBWDOrientationValues.landscapeRight : @(UIDeviceOrientationLandscapeRight), + }; + }); + return orientationMap; +} + +/* + We already have FBWDOrientationValues as orientation descriptions, however the strings are not valid + WebDriver responses. WebDriver can only receive 'portrait' or 'landscape'. So we can pass the keys + through this additional filter to ensure we get one of those. It's essentially a mapping from + FBWDOrientationValues to the valid subset of itself we can return to the client + */ ++ (NSDictionary *)_wdOrientationsMapping +{ + static NSDictionary *orientationMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + orientationMap = + @{ + FBWDOrientationValues.portrait : FBWDOrientationValues.portrait, + FBWDOrientationValues.portraitUpsideDown : FBWDOrientationValues.portrait, + FBWDOrientationValues.landscapeLeft : FBWDOrientationValues.landscapeLeft, + FBWDOrientationValues.landscapeRight : FBWDOrientationValues.landscapeLeft, + }; + }); + return orientationMap; +} + +@end + +#endif diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.h b/WebDriverAgentLib/Commands/FBScreenshotCommands.h new file mode 100644 index 0000000..3f4fa4a --- /dev/null +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenshotCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgentLib/Commands/FBScreenshotCommands.m new file mode 100644 index 0000000..71d6ba5 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreenshotCommands.h" + +#import "XCUIDevice+FBHelpers.h" + +@implementation FBScreenshotCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)], + [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)], + ]; +} + + +#pragma mark - Commands + ++ (id)handleGetScreenshot:(FBRouteRequest *)request +{ + NSError *error; + NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error]; + if (nil == screenshotData) { + return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]); + } + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + return FBResponseWithObject(screenshot); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.h b/WebDriverAgentLib/Commands/FBSessionCommands.h new file mode 100644 index 0000000..95f3f25 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBSessionCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBSessionCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m new file mode 100644 index 0000000..1a9ddf4 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -0,0 +1,586 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBSessionCommands.h" + +#import "FBCapabilities.h" +#import "FBClassChainQueryParser.h" +#import "FBConfiguration.h" +#import "FBExceptions.h" +#import "FBLogger.h" +#import "FBProtocolHelpers.h" +#import "FBRouteRequest.h" +#import "FBSession.h" +#import "FBSettings.h" +#import "FBRuntimeUtils.h" +#import "FBActiveAppDetectionPoint.h" +#import "FBXCodeCompatibility.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIApplication+FBQuiescence.h" +#import "XCUIDevice.h" +#import "XCUIDevice+FBHealthCheck.h" +#import "XCUIDevice+FBHelpers.h" +#import "XCUIApplicationProcessDelay.h" + + +@implementation FBSessionCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)], + [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)], + [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)], + [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)], + [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)], + [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)], + [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)], + [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)], + [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)], + [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)], + + // Health check might modify simulator state so it should only be called in-between testing sessions + [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)], + + // Settings endpoints + [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)], + [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)], + ]; +} + + +#pragma mark - Commands + ++ (id)handleOpenURL:(FBRouteRequest *)request +{ + NSString *urlString = request.arguments[@"url"]; + if (!urlString) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"URL is required" traceback:nil]); + } + NSString* bundleId = request.arguments[@"bundleId"]; + NSNumber* idleTimeoutMs = request.arguments[@"idleTimeoutMs"]; + NSError *error; + if (nil == bundleId) { + if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) { + return FBResponseWithUnknownError(error); + } + } else { + if (![XCUIDevice.sharedDevice fb_openUrl:urlString withApplication:bundleId error:&error]) { + return FBResponseWithUnknownError(error); + } + if (idleTimeoutMs.doubleValue > 0) { + XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleId]; + [app fb_waitUntilStableWithTimeout:FBMillisToSeconds(idleTimeoutMs.doubleValue)]; + } + } + return FBResponseWithOK(); +} + ++ (id)handleCreateSession:(FBRouteRequest *)request +{ + if (nil != FBSession.activeSession) { + [FBSession.activeSession kill]; + } + + NSDictionary *capabilities; + NSError *error; + if (![request.arguments[@"capabilities"] isKindOfClass:NSDictionary.class]) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session" + traceback:nil]); + } + if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]); + } + + [FBConfiguration resetSessionSettings]; + [FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]]; + if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { + [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; + } + NSString *elementResponseAttributes = capabilities[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]; + if (elementResponseAttributes) { + [FBConfiguration setElementResponseAttributes:elementResponseAttributes]; + } + if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) { + [FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; + } + if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) { + [FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]]; + } + if (capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS]) { + if ([capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS] boolValue]) { + [FBConfiguration disableScreenshots]; + } else { + [FBConfiguration enableScreenshots]; + } + } + if (capabilities[FB_CAP_SHOULD_TERMINATE_APP]) { + [FBConfiguration setShouldTerminateApp:[capabilities[FB_CAP_SHOULD_TERMINATE_APP] boolValue]]; + } + NSNumber *delay = capabilities[FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC]; + if ([delay doubleValue] > 0.0) { + [XCUIApplicationProcessDelay setEventLoopHasIdledDelay:[delay doubleValue]]; + } else { + [XCUIApplicationProcessDelay disableEventLoopDelay]; + } + + if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { + FBConfiguration.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]; + } + + if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] || + [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) { + [FBConfiguration forceSimulatorSoftwareKeyboardPresence]; + } + + NSString *bundleID = capabilities[FB_CAP_BUNDLE_ID]; + NSString *initialUrl = capabilities[FB_CAP_INITIAL_URL]; + XCUIApplication *app = nil; + if (bundleID != nil) { + app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID]; + BOOL forceAppLaunch = YES; + if (nil != capabilities[FB_CAP_FORCE_APP_LAUNCH]) { + forceAppLaunch = [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue]; + } + XCUIApplicationState appState = app.state; + BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground; + if (!isAppRunning || (isAppRunning && forceAppLaunch)) { + app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] + || [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue]; + app.launchArguments = (NSArray *)capabilities[FB_CAP_ARGUMENTS] ?: @[]; + app.launchEnvironment = (NSDictionary *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{}; + if (nil != initialUrl) { + if (app.running) { + [app terminate]; + } + id errorResponse = [self openDeepLink:initialUrl + withApplication:bundleID + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + if (nil != errorResponse) { + return errorResponse; + } + } else { + NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]); + } + @try { + [app launch]; + } @catch (NSException *e) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]); + } @finally { + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout(defaultTimeout); + } + } + } + if (!app.running) { + NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID]; + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg + traceback:nil]); + } + } else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) { + if (nil != initialUrl) { + id errorResponse = [self openDeepLink:initialUrl + withApplication:bundleID + timeout:nil]; + if (nil != errorResponse) { + return errorResponse; + } + } else { + [app activate]; + } + } + } + + if (nil != initialUrl && nil == bundleID) { + id errorResponse = [self openDeepLink:initialUrl + withApplication:nil + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + if (nil != errorResponse) { + return errorResponse; + } + } + + if (capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]) { + [FBSession initWithApplication:app + defaultAlertAction:(id)capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]]; + } else { + [FBSession initWithApplication:app]; + } + + if (nil != capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY]) { + FBSession.activeSession.useNativeCachingStrategy = [capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY] boolValue]; + } + + return FBResponseWithObject(FBSessionCommands.sessionInformation); +} + ++ (id)handleSessionAppLaunch:(FBRouteRequest *)request +{ + [request.session launchApplicationWithBundleId:(id)request.arguments[@"bundleId"] + shouldWaitForQuiescence:request.arguments[@"shouldWaitForQuiescence"] + arguments:request.arguments[@"arguments"] + environment:request.arguments[@"environment"]]; + return FBResponseWithOK(); +} + ++ (id)handleSessionAppActivate:(FBRouteRequest *)request +{ + [request.session activateApplicationWithBundleId:(id)request.arguments[@"bundleId"]]; + return FBResponseWithOK(); +} + ++ (id)handleSessionAppTerminate:(FBRouteRequest *)request +{ + BOOL result = [request.session terminateApplicationWithBundleId:(id)request.arguments[@"bundleId"]]; + return FBResponseWithObject(@(result)); +} + ++ (id)handleSessionAppState:(FBRouteRequest *)request +{ + NSUInteger state = [request.session applicationStateWithBundleId:(id)request.arguments[@"bundleId"]]; + return FBResponseWithObject(@(state)); +} + ++ (id)handleGetActiveAppsList:(FBRouteRequest *)request +{ + return FBResponseWithObject([XCUIApplication fb_activeAppsInfo]); +} + ++ (id)handleGetActiveSession:(FBRouteRequest *)request +{ + return FBResponseWithObject(FBSessionCommands.sessionInformation); +} + ++ (id)handleDeleteSession:(FBRouteRequest *)request +{ + [request.session kill]; + return FBResponseWithOK(); +} + ++ (id)handleGetStatus:(FBRouteRequest *)request +{ + // For updatedWDABundleId capability by Appium + NSString *productBundleIdentifier = @"com.facebook.WebDriverAgentRunner"; + NSString *envproductBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"]; + if (envproductBundleIdentifier && [envproductBundleIdentifier length] != 0) { + productBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"]; + } + + NSMutableDictionary *buildInfo = [NSMutableDictionary dictionaryWithDictionary:@{ + @"time" : [self.class buildTimestamp], + @"productBundleIdentifier" : productBundleIdentifier, + }]; + NSString *upgradeTimestamp = NSProcessInfo.processInfo.environment[@"UPGRADE_TIMESTAMP"]; + if (nil != upgradeTimestamp && upgradeTimestamp.length > 0) { + [buildInfo setObject:upgradeTimestamp forKey:@"upgradedAt"]; + } + NSDictionary *infoDict = [[NSBundle bundleForClass:self.class] infoDictionary]; + NSString *version = [infoDict objectForKey:@"CFBundleShortVersionString"]; + if (nil != version) { + [buildInfo setObject:version forKey:@"version"]; + } + + return FBResponseWithObject( + @{ + @"ready" : @YES, + @"message" : @"WebDriverAgent is ready to accept commands", + @"state" : @"success", + @"os" : + @{ + @"name" : [[UIDevice currentDevice] systemName], + @"version" : [[UIDevice currentDevice] systemVersion], + @"sdkVersion": FBSDKVersion() ?: @"unknown", + @"testmanagerdVersion": @(FBTestmanagerdVersion()), + }, + @"ios" : + @{ +#if TARGET_OS_SIMULATOR + @"simulatorVersion" : [[UIDevice currentDevice] systemVersion], +#endif + @"ip" : [XCUIDevice sharedDevice].fb_wifiIPAddress ?: [NSNull null] + }, + @"build" : buildInfo.copy, + @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom] + } + ); +} + ++ (id)handleGetHealthCheck:(FBRouteRequest *)request +{ + if (![[XCUIDevice sharedDevice] fb_healthCheckWithApplication:[XCUIApplication fb_activeApplication]]) { + return FBResponseWithUnknownErrorFormat(@"Health check failed"); + } + return FBResponseWithOK(); +} + ++ (id)handleGetSettings:(FBRouteRequest *)request +{ + return FBResponseWithObject( + @{ + FB_SETTING_USE_COMPACT_RESPONSES: @([FBConfiguration shouldUseCompactResponses]), + FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES: [FBConfiguration elementResponseAttributes], + FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]), + FB_SETTING_MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]), + FB_SETTING_MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]), + FB_SETTING_MJPEG_FIX_ORIENTATION: @([FBConfiguration mjpegShouldFixOrientation]), + FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]), + FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]), + FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]), + FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]), + FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]), + FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]), + FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT: @([FBConfiguration animationCoolOffTimeout]), + FB_SETTING_BOUND_ELEMENTS_BY_INDEX: @([FBConfiguration boundElementsByIndex]), + FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]), + FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication, + FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates, + FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]), + FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector, + FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector, + FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector, + FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"", + FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]), + FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]), + FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]), + FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]), + FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]), + FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]), + FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]), +#if !TARGET_OS_TV + FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation], +#endif + } + ); +} + +// TODO if we get lots more settings, handling them with a series of if-statements will be unwieldy +// and this should be refactored ++ (id)handleSetSettings:(FBRouteRequest *)request +{ + NSDictionary* settings = request.arguments[@"settings"]; + + if (nil != [settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES]) { + [FBConfiguration setShouldUseCompactResponses:[[settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]) { + [FBConfiguration setElementResponseAttributes:(NSString *)[settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]]; + } + if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY]) { + [FBConfiguration setMjpegServerScreenshotQuality:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] unsignedIntegerValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE]) { + [FBConfiguration setMjpegServerFramerate:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE] unsignedIntegerValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY]) { + [FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) { + [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) { + [FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION]) { + [FBConfiguration setKeyboardAutocorrection:[[settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION]) { + [FBConfiguration setKeyboardPrediction:[[settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) { + [FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) { + [FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_USE_FIRST_MATCH]) { + [FBConfiguration setUseFirstMatch:[[settings objectForKey:FB_SETTING_USE_FIRST_MATCH] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX]) { + [FBConfiguration setBoundElementsByIndex:[[settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_REDUCE_MOTION]) { + [FBConfiguration setReduceMotionEnabled:[[settings objectForKey:FB_SETTING_REDUCE_MOTION] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]) { + request.session.defaultActiveApplication = (NSString *)[settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]; + } + if (nil != [settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]) { + NSError *error; + if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)[settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT] + error:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]) { + if ([XCUIElement fb_supportsNonModalElementsInclusion]) { + [FBConfiguration setIncludeNonModalElements:[[settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS] boolValue]]; + } else { + [FBLogger logFmt:@"'%@' settings value cannot be assigned, because non modal elements inclusion is not supported by the current iOS SDK", FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]; + } + } + if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) { + [FBConfiguration setAcceptAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]]; + } + if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) { + [FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]]; + } + if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) { + FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] + forSession:request.session]; + if (status.hasError) { + return FBResponseWithStatus(status); + } + } + if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { + [FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT]) { + [FBConfiguration setAnimationCoolOffTimeout:[[settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] doubleValue]]; + } + if ([[settings objectForKey:FB_SETTING_DEFAULT_ALERT_ACTION] isKindOfClass:NSString.class]) { + request.session.defaultAlertAction = [settings[FB_SETTING_DEFAULT_ALERT_ACTION] lowercaseString]; + } + if (nil != [settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY]) { + [FBConfiguration setMaxTypingFrequency:[[settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) { + [FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) { + [FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]]; + } + +#if !TARGET_OS_TV + if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) { + NSError *error; + if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION] + error:&error]) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + } +#endif + + return [self handleGetSettings:request]; +} + + +#pragma mark - Helpers + ++ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector + forSession:(FBSession *)session +{ + if (0 == [selector length]) { + [FBConfiguration setAutoClickAlertSelector:selector]; + [session disableAlertsMonitor]; + return [FBCommandStatus ok]; + } + + NSError *error; + FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error]; + if (nil == parsedChain) { + return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription + traceback:nil]; + } + [FBConfiguration setAutoClickAlertSelector:selector]; + [session enableAlertsMonitor]; + return [FBCommandStatus ok]; +} + ++ (NSString *)buildTimestamp +{ + return [NSString stringWithFormat:@"%@ %@", + [NSString stringWithUTF8String:__DATE__], + [NSString stringWithUTF8String:__TIME__] + ]; +} + +/** + Return current session information. + This response does not have any active application information. +*/ ++ (NSDictionary *)sessionInformation +{ + return + @{ + @"sessionId" : [FBSession activeSession].identifier ?: NSNull.null, + @"capabilities" : FBSessionCommands.currentCapabilities + }; +} + +/* + Return the device kind as lower case +*/ ++ (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom) userInterfaceIdiom +{ + if (userInterfaceIdiom == UIUserInterfaceIdiomPad) { + return @"ipad"; + } else if (userInterfaceIdiom == UIUserInterfaceIdiomTV) { + return @"apple tv"; + } else if (userInterfaceIdiom == UIUserInterfaceIdiomPhone) { + return @"iphone"; + } + // CarPlay, Mac, Vision UI or unknown are possible + return @"Unknown"; + +} + ++ (NSDictionary *)currentCapabilities +{ + return + @{ + @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom], + @"sdkVersion": [[UIDevice currentDevice] systemVersion] + }; +} + ++(nullable id)openDeepLink:(NSString *)initialUrl + withApplication:(nullable NSString *)bundleID + timeout:(nullable NSNumber *)timeout +{ + NSError *openError; + NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); + if (nil != timeout) { + _XCTSetApplicationStateTimeout([timeout doubleValue]); + } + @try { + BOOL result = nil == bundleID + ? [XCUIDevice.sharedDevice fb_openUrl:initialUrl + error:&openError] + : [XCUIDevice.sharedDevice fb_openUrl:initialUrl + withApplication:(id)bundleID + error:&openError]; + if (result) { + return nil; + } + NSString *errorMsg = [NSString stringWithFormat:@"Cannot open the URL %@ with the %@ application. Original error: %@", + initialUrl, bundleID ?: @"default", openError.localizedDescription]; + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]); + } @finally { + if (nil != timeout) { + _XCTSetApplicationStateTimeout(defaultTimeout); + } + } +} + +@end diff --git a/WebDriverAgentLib/Commands/FBTouchActionCommands.h b/WebDriverAgentLib/Commands/FBTouchActionCommands.h new file mode 100644 index 0000000..d9b84fc --- /dev/null +++ b/WebDriverAgentLib/Commands/FBTouchActionCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBTouchActionCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBTouchActionCommands.m b/WebDriverAgentLib/Commands/FBTouchActionCommands.m new file mode 100644 index 0000000..7788175 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBTouchActionCommands.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTouchActionCommands.h" + +#import "FBRoute.h" +#import "FBRouteRequest.h" +#import "FBSession.h" +#import "XCUIApplication+FBTouchAction.h" + +@implementation FBTouchActionCommands + +#pragma mark - + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)], + ]; +} + +#pragma mark - Commands + ++ (id)handlePerformW3CTouchActions:(FBRouteRequest *)request +{ + XCUIApplication *application = request.session.activeApplication; + NSArray *actions = (NSArray *)request.arguments[@"actions"]; + NSError *error; + if (![application fb_performW3CActions:actions elementCache:request.session.elementCache error:&error]) { + return FBResponseWithUnknownError(error); + } + return FBResponseWithOK(); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.h b/WebDriverAgentLib/Commands/FBTouchIDCommands.h new file mode 100644 index 0000000..ffcf2e8 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBTouchIDCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.m b/WebDriverAgentLib/Commands/FBTouchIDCommands.m new file mode 100644 index 0000000..9594dd6 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTouchIDCommands.h" + +#import "FBRouteRequest.h" + +#import "XCUIDevice+FBHelpers.h" + +@implementation FBTouchIDCommands + ++ (NSArray *)routes +{ + return @[ + [[FBRoute POST:@"/wda/touch_id"] respondWithBlock: ^ id (FBRouteRequest *request) { + BOOL isMatch = [request.arguments[@"match"] boolValue]; + if (![[XCUIDevice sharedDevice] fb_fingerTouchShouldMatch:isMatch]) { + return FBResponseWithUnknownErrorFormat(@"Cannot perform Touch Id %@match", isMatch ? @"" : @"non-"); + } + return FBResponseWithOK(); + }], + ]; +} + +@end diff --git a/WebDriverAgentLib/Commands/FBUnknownCommands.h b/WebDriverAgentLib/Commands/FBUnknownCommands.h new file mode 100644 index 0000000..3e37d78 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBUnknownCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBUnknownCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBUnknownCommands.m b/WebDriverAgentLib/Commands/FBUnknownCommands.m new file mode 100644 index 0000000..7fc35b9 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBUnknownCommands.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBUnknownCommands.h" + +#import "FBRouteRequest.h" + +@implementation FBUnknownCommands + +#pragma mark - + ++ (BOOL)shouldRegisterAutomatically +{ + return NO; +} + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute GET:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute POST:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute PUT:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute DELETE:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)] + ]; +} + ++ (id)unhandledHandler:(FBRouteRequest *)request +{ + return FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:[NSString stringWithFormat:@"Unhandled endpoint: %@ with parameters %@", request.URL, request.parameters] + traceback:nil]); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBVideoCommands.h b/WebDriverAgentLib/Commands/FBVideoCommands.h new file mode 100644 index 0000000..a3e7a0a --- /dev/null +++ b/WebDriverAgentLib/Commands/FBVideoCommands.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBVideoCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBVideoCommands.m b/WebDriverAgentLib/Commands/FBVideoCommands.m new file mode 100644 index 0000000..a5c36a5 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBVideoCommands.m @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBVideoCommands.h" + +#import "FBRouteRequest.h" +#import "FBScreenRecordingContainer.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" +#import "FBSession.h" +#import "FBXCTestDaemonsProxy.h" + +const NSUInteger DEFAULT_FPS = 24; +const NSUInteger DEFAULT_CODEC = 0; + +@implementation FBVideoCommands + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/wda/video/start"] respondWithTarget:self action:@selector(handleStartVideoRecording:)], + [[FBRoute POST:@"/wda/video/stop"] respondWithTarget:self action:@selector(handleStopVideoRecording:)], + [[FBRoute GET:@"/wda/video"] respondWithTarget:self action:@selector(handleGetVideoRecording:)], + + [[FBRoute POST:@"/wda/video/start"].withoutSession respondWithTarget:self action:@selector(handleStartVideoRecording:)], + [[FBRoute POST:@"/wda/video/stop"].withoutSession respondWithTarget:self action:@selector(handleStopVideoRecording:)], + [[FBRoute GET:@"/wda/video"].withoutSession respondWithTarget:self action:@selector(handleGetVideoRecording:)], + ]; +} + ++ (id)handleStartVideoRecording:(FBRouteRequest *)request +{ + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil != activeScreenRecording) { + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]); + } + + NSNumber *fps = (NSNumber *)request.arguments[@"fps"] ?: @(DEFAULT_FPS); + NSNumber *codec = (NSNumber *)request.arguments[@"codec"] ?: @(DEFAULT_CODEC); + FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:fps.integerValue + codec:codec.longLongValue]; + NSError *error; + FBScreenRecordingPromise* promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest + error:&error]; + if (nil == promise) { + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithUnknownError(error); + } + [FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise + fps:fps.integerValue + codec:codec.longLongValue]; + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary]); +} + ++ (id)handleStopVideoRecording:(FBRouteRequest *)request +{ + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil == activeScreenRecording) { + return FBResponseWithOK(); + } + + NSUUID *recordingId = activeScreenRecording.identifier; + NSDictionary *response = [FBScreenRecordingContainer.sharedInstance toDictionary]; + NSError *error; + if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:recordingId error:&error]) { + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithUnknownError(error); + } + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithObject(response); +} + ++ (id)handleGetVideoRecording:(FBRouteRequest *)request +{ + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]); +} + +@end diff --git a/WebDriverAgentLib/FBAlert.h b/WebDriverAgentLib/FBAlert.h new file mode 100644 index 0000000..8e9ec8c --- /dev/null +++ b/WebDriverAgentLib/FBAlert.h @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class XCUIApplication; +@class XCUIElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + Alert helper class that abstracts alert handling + */ +@interface FBAlert : NSObject + +/** + Creates alert helper for given application + + @param application The application that contains the alert + */ ++ (instancetype)alertWithApplication:(XCUIApplication *)application; + +/** + Creates alert helper for given application + + @param element The element which represents the alert + */ ++ (instancetype)alertWithElement:(XCUIElement *)element; + +/** + Determines whether alert is present + */ +- (BOOL)isPresent; + +/** + Gets the labels of the buttons visible in the alert + */ +- (nullable NSArray *)buttonLabels; + +/** + Returns alert's title and description separated by new lines + */ +- (nullable NSString *)text; + +/** + Accepts alert, if present + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)acceptWithError:(NSError **)error; + +/** + Dismisses alert, if present + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)dismissWithError:(NSError **)error; + +/** + Clicks on an alert button, if present + + @param label The label of the button on which to click. + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation suceeds, otherwise NO. + */ +- (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error; + +/** + XCUElement that represents alert + */ +- (nullable XCUIElement *)alertElement; + +/** + Types a text into an input inside the alert container, if it is present + + @param text the text to type + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +- (BOOL)typeText:(NSString *)text error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/FBAlert.m b/WebDriverAgentLib/FBAlert.m new file mode 100644 index 0000000..2e2de76 --- /dev/null +++ b/WebDriverAgentLib/FBAlert.m @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAlert.h" + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBAlert.h" +#import "XCUIElement+FBClassChain.h" +#import "XCUIElement+FBTyping.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" + + +@interface FBAlert () +@property (nonatomic, strong) XCUIApplication *application; +@property (nonatomic, strong, nullable) XCUIElement *element; +@end + +@implementation FBAlert + ++ (instancetype)alertWithApplication:(XCUIApplication *)application +{ + FBAlert *alert = [FBAlert new]; + alert.application = application; + return alert; +} + ++ (instancetype)alertWithElement:(XCUIElement *)element +{ + FBAlert *alert = [FBAlert new]; + alert.element = element; + alert.application = element.application; + return alert; +} + +- (BOOL)isPresent +{ + @try { + if (nil == self.alertElement) { + return NO; + } + [self.alertElement fb_customSnapshot]; + return YES; + } @catch (NSException *) { + return NO; + } +} + +- (BOOL)notPresentWithError:(NSError **)error +{ + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"No alert is open"] + buildError:error]; +} + ++ (BOOL)isSafariWebAlertWithSnapshot:(id)snapshot +{ + if (snapshot.elementType != XCUIElementTypeOther) { + return NO; + } + + FBXCElementSnapshotWrapper *snapshotWrapper = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + id application = [snapshotWrapper fb_parentMatchingType:XCUIElementTypeApplication]; + return nil != application && [application.label isEqualToString:FB_SAFARI_APP_NAME]; +} + +- (NSString *)text +{ + if (!self.isPresent) { + return nil; + } + + NSMutableArray *resultText = [NSMutableArray array]; + id snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot]; + [snapshot enumerateDescendantsUsingBlock:^(id descendant) { + XCUIElementType elementType = descendant.elementType; + if (!(elementType == XCUIElementTypeTextView || elementType == XCUIElementTypeStaticText)) { + return; + } + + FBXCElementSnapshotWrapper *descendantWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant]; + if (elementType == XCUIElementTypeStaticText + && nil != [descendantWrapper fb_parentMatchingType:XCUIElementTypeButton]) { + return; + } + + NSString *text = descendantWrapper.wdLabel ?: descendantWrapper.wdValue; + if (isSafariAlert && nil != descendant.parent) { + FBXCElementSnapshotWrapper *descendantParentWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant.parent]; + NSString *parentText = descendantParentWrapper.wdLabel ?: descendantParentWrapper.wdValue; + if ([parentText isEqualToString:text]) { + // Avoid duplicated texts on Safari alerts + return; + } + } + + if (nil != text) { + [resultText addObject:[NSString stringWithFormat:@"%@", text]]; + } + }]; + return [resultText componentsJoinedByString:@"\n"]; +} + +- (BOOL)typeText:(NSString *)text error:(NSError **)error +{ + if (!self.isPresent) { + return [self notPresentWithError:error]; + } + + NSPredicate *textCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu}", + XCUIElementTypeTextField, XCUIElementTypeSecureTextField]; + NSArray *dstFields = [[self.alertElement descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:textCollectorPredicate].allElementsBoundByIndex; + if (dstFields.count > 1) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The alert contains more than one input field"] + buildError:error]; + } + if (0 == dstFields.count) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The alert contains no input fields"] + buildError:error]; + } + return [dstFields.firstObject fb_typeText:text + shouldClear:YES + error:error]; +} + +- (NSArray *)buttonLabels +{ + if (!self.isPresent) { + return nil; + } + + NSMutableArray *labels = [NSMutableArray array]; + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + [alertSnapshot enumerateDescendantsUsingBlock:^(id descendant) { + if (descendant.elementType != XCUIElementTypeButton) { + return; + } + NSString *label = [FBXCElementSnapshotWrapper ensureWrapped:descendant].wdLabel; + if (nil != label) { + [labels addObject:[NSString stringWithFormat:@"%@", label]]; + } + }]; + return labels.copy; +} + +- (BOOL)acceptWithError:(NSError **)error +{ + if (!self.isPresent) { + return [self notPresentWithError:error]; + } + + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + XCUIElement *acceptButton = nil; + if (FBConfiguration.acceptAlertButtonSelector.length) { + NSString *errorReason = nil; + @try { + acceptButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.acceptAlertButtonSelector + shouldReturnAfterFirstMatch:YES] firstObject]; + } @catch (NSException *ex) { + errorReason = ex.reason; + } + if (nil == acceptButton) { + [FBLogger logFmt:@"Cannot find any match for Accept alert button using the class chain selector '%@'", + FBConfiguration.acceptAlertButtonSelector]; + if (nil != errorReason) { + [FBLogger logFmt:@"Original error: %@", errorReason]; + } + [FBLogger log:@"Will fallback to the default button location algorithm"]; + } + } + if (nil == acceptButton) { + NSArray *buttons = [self.alertElement.fb_query + descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex; + acceptButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot]) + ? buttons.lastObject + : buttons.firstObject; + } + if (nil == acceptButton) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Failed to find accept button for alert: %@", self.alertElement] + buildError:error]; + } + [acceptButton tap]; + return YES; +} + +- (BOOL)dismissWithError:(NSError **)error +{ + if (!self.isPresent) { + return [self notPresentWithError:error]; + } + + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + XCUIElement *dismissButton = nil; + if (FBConfiguration.dismissAlertButtonSelector.length) { + NSString *errorReason = nil; + @try { + dismissButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.dismissAlertButtonSelector + shouldReturnAfterFirstMatch:YES] firstObject]; + } @catch (NSException *ex) { + errorReason = ex.reason; + } + if (nil == dismissButton) { + [FBLogger logFmt:@"Cannot find any match for Dismiss alert button using the class chain selector '%@'", + FBConfiguration.dismissAlertButtonSelector]; + if (nil != errorReason) { + [FBLogger logFmt:@"Original error: %@", errorReason]; + } + [FBLogger log:@"Will fallback to the default button location algorithm"]; + } + } + if (nil == dismissButton) { + NSArray *buttons = [self.alertElement.fb_query + descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex; + dismissButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot]) + ? buttons.firstObject + : buttons.lastObject; + } + + if (nil == dismissButton) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Failed to find dismiss button for alert: %@", self.alertElement] + buildError:error]; + } + [dismissButton tap]; + return YES; +} + +- (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error +{ + if (!self.isPresent) { + return [self notPresentWithError:error]; + } + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label == %@", label]; + XCUIElement *requestedButton = [[self.alertElement descendantsMatchingType:XCUIElementTypeButton] + matchingPredicate:predicate].allElementsBoundByIndex.firstObject; + if (!requestedButton) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Failed to find button with label '%@' for alert: %@", label, self.alertElement] + buildError:error]; + } + [requestedButton tap]; + return YES; +} + +- (XCUIElement *)alertElement +{ + if (nil == self.element) { + XCUIApplication *systemApp = XCUIApplication.fb_systemApplication; + if ([systemApp fb_isSameAppAs:self.application]) { + self.element = systemApp.fb_alertElement; + } else { + self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement; + } + } + return self.element; +} + +@end diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist new file mode 100644 index 0000000..e5ff84a --- /dev/null +++ b/WebDriverAgentLib/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.1.2 + CFBundleSignature + ???? + CFBundleVersion + 10.1.2 + NSPrincipalClass + + + \ No newline at end of file diff --git a/WebDriverAgentLib/Routing/FBCommandHandler.h b/WebDriverAgentLib/Routing/FBCommandHandler.h new file mode 100644 index 0000000..c4ac83b --- /dev/null +++ b/WebDriverAgentLib/Routing/FBCommandHandler.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Protocol for Classes to declare intent to implement responses to commands + */ +@protocol FBCommandHandler + +/** + * Should return map of FBRouteCommandHandler block with keys as supported routes + * + * @return map an NSArray of routes. + */ ++ (NSArray *)routes; + +@optional +/** + * @return BOOL deciding if class should be added to route handlers automatically, default (if not implemented) is YES + */ ++ (BOOL)shouldRegisterAutomatically; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBCommandStatus.h b/WebDriverAgentLib/Routing/FBCommandStatus.h new file mode 100644 index 0000000..0929ff1 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBCommandStatus.h @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBCommandStatus : NSObject + +@property (nonatomic, nullable, readonly) id value; +@property (nonatomic, nullable, readonly) NSString* error; +@property (nonatomic, nullable, readonly) NSString* message; +@property (nonatomic, nullable, readonly) NSString* traceback; +@property (nonatomic, readonly) HTTPStatusCode statusCode; +@property (nonatomic, readonly) BOOL hasError; + ++ (instancetype)ok; + ++ (instancetype)okWithValue:(nullable id)value; + ++ (instancetype)unknownErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)unsupportedOperationErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)unableToCaptureScreenErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)noSuchElementErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)invalidElementStateErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)invalidArgumentErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)staleElementReferenceErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)invalidSelectorErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)noAlertOpenErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)unexpectedAlertOpenErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)notImplementedErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)sessionNotCreatedError:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)invalidCoordinatesErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)unknownCommandErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)timeoutErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)elementNotVisibleErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + ++ (instancetype)noSuchDriverErrorWithMessage:(nullable NSString *)message + traceback:(nullable NSString *)traceback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBCommandStatus.m b/WebDriverAgentLib/Routing/FBCommandStatus.m new file mode 100644 index 0000000..fb5d243 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBCommandStatus.m @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBCommandStatus.h" + +static NSString *const FB_UNKNOWN_ERROR = @"unknown error"; +static const HTTPStatusCode FB_UNKNOWN_ERROR_CODE = kHTTPStatusCodeInternalServerError; +static NSString *const FB_UNKNOWN_ERROR_MSG = @"An unknown server-side error occurred while processing the command"; + +static NSString *const FB_UNABLE_TO_CAPTURE_ERROR = @"unable to capture screen"; +static const HTTPStatusCode FB_UNABLE_TO_CAPTURE_ERROR_CODE = kHTTPStatusCodeInternalServerError; +static NSString *const FB_UNABLE_TO_CAPTURE_MSG = @"A screen capture was made impossible"; + +static NSString *const FB_NO_SUCH_ELEMENT_ERROR = @"no such element"; +static const HTTPStatusCode FB_NO_SUCH_ELEMENT_ERROR_CODE = kHTTPStatusCodeNotFound; +static NSString *const FB_NO_SUCH_ELEMENT_MSG = @"An element could not be located on the page using the given search parameters"; + +static NSString *const FB_INVALID_ELEMENT_STATE_ERROR = @"invalid element state"; +static const HTTPStatusCode FB_INVALID_ELEMENT_STATE_ERROR_CODE = kHTTPStatusCodeBadRequest; +static NSString *const FB_INVALID_ELEMENT_STATE_MSG = @"An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)"; + +static NSString *const FB_INVALID_ARGUMENT_ERROR = @"invalid argument"; +static const HTTPStatusCode FB_INVALID_ARGUMENT_ERROR_CODE = kHTTPStatusCodeBadRequest; +static NSString *const FB_INVALID_ARGUMENT_MSG = @"The arguments passed to the command are either invalid or malformed"; + +static NSString *const FB_STALE_ELEMENT_REF_ERROR = @"stale element reference"; +static const HTTPStatusCode FB_STALE_ELEMENT_REF_ERROR_CODE = kHTTPStatusCodeNotFound; +static NSString *const FB_STALE_ELEMENT_REF_MSG = @"An element command failed because the referenced element is no longer attached to the DOM"; + +static NSString *const FB_INVALID_SELECTOR_ERROR = @"invalid selector"; +static const HTTPStatusCode FB_INVALID_SELECTOR_ERROR_CODE = kHTTPStatusCodeBadRequest; +static NSString *const FB_INVALID_SELECTOR_MSG = @"Argument was an invalid selector (e.g. XPath/Class Chain)"; + +static NSString *const FB_NO_ALERT_OPEN_ERROR = @"no such alert"; +static const HTTPStatusCode FB_NO_ALERT_OPEN_ERROR_CODE = kHTTPStatusCodeNotFound; +static NSString *const FB_NO_ALERT_OPEN_MSG = @"An attempt was made to operate on a modal dialog when one was not open"; + +static NSString *const FB_UNEXPECTED_ALERT_OPEN_ERROR = @"unexpected alert open"; +static const HTTPStatusCode FB_UNEXPECTED_ALERT_OPEN_ERROR_CODE = kHTTPStatusCodeInternalServerError; +static NSString *const FB_UNEXPECTED_ALERT_OPEN_MSG = @"A modal dialog was open, blocking this operation"; + +static NSString *const FB_NOT_IMPLEMENTED_ERROR = @"unknown method"; +static const HTTPStatusCode FB_NOT_IMPLEMENTED_ERROR_CODE = kHTTPStatusCodeMethodNotAllowed; +static NSString *const FB_NOT_IMPLEMENTED_MSG = @"Method is not implemented"; + +static NSString *const FB_SESSION_NOT_CREATED_ERROR = @"session not created"; +static const HTTPStatusCode FB_SESSION_NOT_CREATED_ERROR_CODE = kHTTPStatusCodeInternalServerError; +static NSString *const FB_SESSION_NOT_CREATED_MSG = @"A new session could not be created"; + +static NSString *const FB_INVALID_COORDINATES_ERROR = @"invalid coordinates"; +static const HTTPStatusCode FB_INVALID_COORDINATES_ERROR_CODE = kHTTPStatusCodeBadRequest; +static NSString *const FB_INVALID_COORDINATES_MSG = @"The coordinates provided to an interactions operation are invalid"; + +static NSString *const FB_UNSUPPORTED_OPERATION_ERROR = @"unsupported operation"; +static const HTTPStatusCode FB_UNSUPPORTED_OPERATION_ERROR_CODE = kHTTPStatusCodeInternalServerError; +static NSString *const FB_UNSUPPORTED_OPERATION_ERROR_MSG = @"The requested operation is not supported"; + +static NSString *const FB_UNKNOWN_COMMAND_ERROR = @"unknown command"; +static const HTTPStatusCode FB_UNKNOWN_COMMAND_ERROR_CODE = kHTTPStatusCodeNotFound; +static NSString *const FB_UNKNOWN_COMMAND_MSG = @"The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource"; + +static NSString *const FB_TIMEOUT_ERROR = @"timeout"; +static const HTTPStatusCode FB_TIMEOUT_ERROR_CODE = kHTTPStatusCodeRequestTimeout; +static NSString *const FB_TIMEOUT_MSG = @"An operation did not complete before its timeout expired"; + +static NSString *const FB_ELEMENT_NOT_VISIBLE_ERROR = @"element not visible"; +static const HTTPStatusCode FB_ELEMENT_NOT_VISIBLE_ERROR_CODE = kHTTPStatusCodeBadRequest; +static NSString *const FB_ELEMENT_NOT_VISIBLE_MSG = @"An element command could not be completed because the element is not visible on the page"; + +static NSString *const FB_NO_SUCH_DRIVER_ERROR = @"invalid session id"; +static const HTTPStatusCode FB_NO_SUCH_DRIVER_ERROR_CODE = kHTTPStatusCodeNotFound; +static NSString *const FB_NO_SUCH_DRIVER_MSG = @"A session is either terminated or not started"; + + +@implementation FBCommandStatus + +- (instancetype)initWithValue:(nullable id)value +{ + self = [super init]; + if (self) { + _value = value; + _message = nil; + _error = nil; + _traceback = nil; + _statusCode = kHTTPStatusCodeOK; + } + return self; +} + +- (instancetype)initWithError:(NSString *)error + statusCode:(HTTPStatusCode)statusCode + message:(NSString *)message + traceback:(nullable NSString *)traceback +{ + self = [super init]; + if (self) { + _error = error; + _statusCode = statusCode; + _message = message; + _traceback = traceback; + _value = nil; + } + return self; +} + +- (BOOL)hasError +{ + return self.statusCode != kHTTPStatusCodeOK; +} + ++ (instancetype)ok +{ + return [[FBCommandStatus alloc] initWithValue:nil]; +} + ++ (instancetype)okWithValue:(id)value +{ + return [[FBCommandStatus alloc] initWithValue:value]; +} + ++ (instancetype)unknownErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_UNKNOWN_ERROR + statusCode:FB_UNKNOWN_ERROR_CODE + message:message ?: FB_UNKNOWN_ERROR_MSG + traceback:traceback]; +} + ++ (instancetype)unsupportedOperationErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_UNSUPPORTED_OPERATION_ERROR + statusCode:FB_UNSUPPORTED_OPERATION_ERROR_CODE + message:message ?: FB_UNSUPPORTED_OPERATION_ERROR_MSG + traceback:traceback]; +} + ++ (instancetype)unableToCaptureScreenErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_UNABLE_TO_CAPTURE_ERROR + statusCode:FB_UNABLE_TO_CAPTURE_ERROR_CODE + message:message ?: FB_UNABLE_TO_CAPTURE_MSG + traceback:traceback]; +} + ++ (instancetype)noSuchElementErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_NO_SUCH_ELEMENT_ERROR + statusCode:FB_NO_SUCH_ELEMENT_ERROR_CODE + message:message ?: FB_NO_SUCH_ELEMENT_MSG + traceback:traceback]; +} + ++ (instancetype)invalidElementStateErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_INVALID_ELEMENT_STATE_ERROR + statusCode:FB_INVALID_ELEMENT_STATE_ERROR_CODE + message:message ?: FB_INVALID_ELEMENT_STATE_MSG + traceback:traceback]; +} + ++ (instancetype)invalidArgumentErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_INVALID_ARGUMENT_ERROR + statusCode:FB_INVALID_ARGUMENT_ERROR_CODE + message:message ?: FB_INVALID_ARGUMENT_MSG + traceback:traceback]; +} + ++ (instancetype)staleElementReferenceErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_STALE_ELEMENT_REF_ERROR + statusCode:FB_STALE_ELEMENT_REF_ERROR_CODE + message:message ?: FB_STALE_ELEMENT_REF_MSG + traceback:traceback]; +} + ++ (instancetype)invalidSelectorErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_INVALID_SELECTOR_ERROR + statusCode:FB_INVALID_SELECTOR_ERROR_CODE + message:message ?: FB_INVALID_SELECTOR_MSG + traceback:traceback]; +} + ++ (instancetype)noAlertOpenErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_NO_ALERT_OPEN_ERROR + statusCode:FB_NO_ALERT_OPEN_ERROR_CODE + message:message ?: FB_NO_ALERT_OPEN_MSG + traceback:traceback]; +} + ++ (instancetype)unexpectedAlertOpenErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_UNEXPECTED_ALERT_OPEN_ERROR + statusCode:FB_UNEXPECTED_ALERT_OPEN_ERROR_CODE + message:message ?: FB_UNEXPECTED_ALERT_OPEN_MSG + traceback:traceback]; +} + ++ (instancetype)notImplementedErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_NOT_IMPLEMENTED_ERROR + statusCode:FB_NOT_IMPLEMENTED_ERROR_CODE + message:message ?: FB_NOT_IMPLEMENTED_MSG + traceback:traceback]; +} + ++ (instancetype)sessionNotCreatedError:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_SESSION_NOT_CREATED_ERROR + statusCode:FB_SESSION_NOT_CREATED_ERROR_CODE + message:message ?: FB_SESSION_NOT_CREATED_MSG + traceback:traceback]; +} + ++ (instancetype)invalidCoordinatesErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_INVALID_COORDINATES_ERROR + statusCode:FB_INVALID_COORDINATES_ERROR_CODE + message:message ?: FB_INVALID_COORDINATES_MSG + traceback:traceback]; +} + ++ (instancetype)unknownCommandErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_UNKNOWN_COMMAND_ERROR + statusCode:FB_UNKNOWN_COMMAND_ERROR_CODE + message:message ?: FB_UNKNOWN_COMMAND_MSG + traceback:traceback]; +} + ++ (instancetype)timeoutErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_TIMEOUT_ERROR + statusCode:FB_TIMEOUT_ERROR_CODE + message:message ?: FB_TIMEOUT_MSG + traceback:traceback]; +} + ++ (instancetype)elementNotVisibleErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_ELEMENT_NOT_VISIBLE_ERROR + statusCode:FB_ELEMENT_NOT_VISIBLE_ERROR_CODE + message:message ?: FB_ELEMENT_NOT_VISIBLE_MSG + traceback:traceback]; +} + ++ (instancetype)noSuchDriverErrorWithMessage:(NSString *)message + traceback:(NSString *)traceback +{ + return [[FBCommandStatus alloc] initWithError:FB_NO_SUCH_DRIVER_ERROR + statusCode:FB_NO_SUCH_DRIVER_ERROR_CODE + message:message ?: FB_NO_SUCH_DRIVER_MSG + traceback:traceback]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBElement.h b/WebDriverAgentLib/Routing/FBElement.h new file mode 100644 index 0000000..b4e6a75 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBElement.h @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Protocol that should be implemented by class that can return element properties defined in WebDriver Spec + */ +@protocol FBElement + +/*! Element's frame in normalized (rounded dimensions without Infinity values) CGRect format */ +@property (nonatomic, readonly, assign) CGRect wdFrame; + +/*! Represents the element's frame as a CGRect, preserving the actual values. */ +@property (nonatomic, readonly, assign) CGRect wdNativeFrame; + +/*! Element's wsFrame in NSDictionary format */ +@property (nonatomic, readonly, copy) NSDictionary *wdRect; + +/*! Element's name */ +@property (nonatomic, readonly, copy, nullable) NSString *wdName; + +/*! Element's label */ +@property (nonatomic, readonly, copy, nullable) NSString *wdLabel; + +/*! Element's selected state */ +@property (nonatomic, readonly, getter = isWDSelected) BOOL wdSelected; + +/*! Element's type */ +@property (nonatomic, readonly, copy) NSString *wdType; + +/*! Element's accessibility traits as a comma-separated string */ +@property (nonatomic, readonly, copy) NSString *wdTraits; + +/*! Element's value */ +@property (nonatomic, readonly, strong, nullable) NSString *wdValue; + +/*! Element's unique identifier */ +@property (nonatomic, readonly, copy, nullable) NSString *wdUID; + +/*! Whether element is enabled */ +@property (nonatomic, readonly, getter = isWDEnabled) BOOL wdEnabled; + +/*! Whether element is visible */ +@property (nonatomic, readonly, getter = isWDVisible) BOOL wdVisible; + +/*! Whether element is accessible */ +@property (nonatomic, readonly, getter = isWDAccessible) BOOL wdAccessible; + +/*! Whether element is an accessibility container (contains children of any depth that are accessible) */ +@property (nonatomic, readonly, getter = isWDAccessibilityContainer) BOOL wdAccessibilityContainer; + +/*! Whether element is focused */ +@property (nonatomic, readonly, getter = isWDFocused) BOOL wdFocused; + +/*! Whether element is hittable */ +@property (nonatomic, readonly, getter = isWDHittable) BOOL wdHittable; + +/*! Element's index relatively to its parent. Starts from zero */ +@property (nonatomic, readonly) NSUInteger wdIndex; + +/*! Element's placeholder value */ +@property (nonatomic, readonly, copy, nullable) NSString *wdPlaceholderValue; + +/*! Element's minimum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMinValue; + +/*! Element's maximum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMaxValue; + +/** + Returns value of given property specified in WebDriver Spec + Check the FBElement protocol to get list of supported attributes. + This method also supports shortcuts, like wdName == name, wdValue == value. + + @param name WebDriver Spec property name + @return the corresponding property value + @throws FBUnknownAttributeException if there is no matching attribute defined in FBElement protocol + */ +- (nullable id)fb_valueForWDAttributeName:(NSString *__nullable)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBElementCache.h b/WebDriverAgentLib/Routing/FBElementCache.h new file mode 100644 index 0000000..1c7eb47 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBElementCache.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class XCUIElement; + +NS_ASSUME_NONNULL_BEGIN + +// This constant defines the size of the element cache, which puts an upper limit +// on the amount of elements which can be stored in the cache. +// Based on the data in https://github.com/facebook/WebDriverAgent/pull/896, each +// element consumes about 100KB of memory; so 1024 elements would consume 100 MB of +// memory. +extern const int ELEMENT_CACHE_SIZE; + +@interface FBElementCache : NSObject + +/** + Stores element in cache + + @param element element to store + @return element's uuid or nil in case the element uid cannnot be extracted + */ +- (nullable NSString *)storeElement:(XCUIElement *)element; + +/** + Returns cached element resolved with default snapshot attributes + + @param uuid uuid of element to fetch + @return element + @throws FBInvalidArgumentException if uuid is nil + */ +- (XCUIElement *)elementForUUID:(NSString *)uuid; + +/** + Returns cached element resolved with default snapshot attributes + + @param uuid uuid of element to fetch + @param checkStaleness Whether to throw FBStaleElementException if the found element is not present in DOM anymore + @return element + @throws FBStaleElementException if `checkStaleness` is enabled + @throws FBInvalidArgumentException if uuid is nil + */ +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness; + +/** + Checks element existence in the cache + + @returns YES if the element with the given UUID is present in cache + */ +- (BOOL)hasElementWithUUID:(nullable NSString *)uuid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBElementCache.m b/WebDriverAgentLib/Routing/FBElementCache.m new file mode 100644 index 0000000..7566242 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBElementCache.m @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementCache.h" + +#import "LRUCache.h" +#import "FBAlert.h" +#import "FBExceptions.h" +#import "FBXCodeCompatibility.h" +#import "XCTestPrivateSymbols.h" +#import "XCUIElement.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBResolve.h" +#import "XCUIElementQuery.h" + +const int ELEMENT_CACHE_SIZE = 1024; + +@interface FBElementCache () +@property (nonatomic, strong) LRUCache *elementCache; +@end + +@implementation FBElementCache + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return nil; + } + _elementCache = [[LRUCache alloc] initWithCapacity:ELEMENT_CACHE_SIZE]; + return self; +} + +- (NSString *)storeElement:(XCUIElement *)element +{ + NSString *uuid = element.fb_cacheId; + if (nil == uuid) { + return nil; + } + @synchronized (self.elementCache) { + [self.elementCache setObject:element forKey:uuid]; + } + return uuid; +} + +- (XCUIElement *)elementForUUID:(NSString *)uuid +{ + return [self elementForUUID:uuid checkStaleness:NO]; +} + +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness +{ + if (!uuid) { + NSString *reason = [NSString stringWithFormat:@"Cannot extract cached element for UUID: %@", uuid]; + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; + } + + XCUIElement *element; + @synchronized (self.elementCache) { + element = [self.elementCache objectForKey:uuid]; + } + if (nil == element) { + NSString *reason = [NSString stringWithFormat:@"The element identified by \"%@\" is either not present or it has expired from the internal cache. Try to find it again", uuid]; + @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; + } + if (checkStaleness) { + @try { + [element fb_standardSnapshot]; + } @catch (NSException *exception) { + // if the snapshot method threw FBStaleElementException (implying the element is stale) we need to explicitly remove it from the cache, PR: https://github.com/appium/WebDriverAgent/pull/985 + if ([exception.name isEqualToString:FBStaleElementException]) { + @synchronized (self.elementCache) { + [self.elementCache removeObjectForKey:uuid]; + } + } + @throw exception; + } + } + return element; +} + +- (BOOL)hasElementWithUUID:(NSString *)uuid +{ + if (nil == uuid) { + return NO; + } + @synchronized (self.elementCache) { + return nil != [self.elementCache objectForKey:(NSString *)uuid]; + } +} + +@end diff --git a/WebDriverAgentLib/Routing/FBElementUtils.h b/WebDriverAgentLib/Routing/FBElementUtils.h new file mode 100644 index 0000000..0c4de4b --- /dev/null +++ b/WebDriverAgentLib/Routing/FBElementUtils.h @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@protocol FBXCAccessibilityElement; + +NS_ASSUME_NONNULL_BEGIN + +/*! Notification used to notify about unknown attribute name */ +extern NSString *const FBUnknownAttributeException; + +@interface FBElementUtils : NSObject + +/** + Returns property name defined by FBElement protocol for the given WebDriver Spec property name. + Check the FBElement protocol to get list of supported attributes. + This method also supports shortcuts, like wdName == name, wdValue == value. + In case the corresponding attribute has a getter defined then the name of the getter witll be returned instead, + which makes this method compatible with KVO lookup + + @param name WebDriver Spec property name + @return the corresponding property name + @throws FBUnknownAttributeException if there is no matching attribute defined in FBElement protocol + */ ++ (NSString *)wdAttributeNameForAttributeName:(NSString *)name; + +/** + Collects all the unique element types from an array of elements. + + @param elements array of elements + @return set of unique element types (XCUIElementType items) or an empty set in case the input is empty + */ ++ (NSSet *)uniqueElementTypesWithElements:(NSArray> *)elements; + +/** + Returns mapping of all possible FBElement protocol properties aliases + + @return dictionary of matching property aliases with their real names as values or getter method names if exist + for KVO lookup + */ ++ (NSDictionary *)wdAttributeNamesMapping; + +/** + Gets the unique identifier of the particular XCAccessibilityElement instance in form of UUIDv4. + + @param element accessiblity element instance + @return the unique element identifier or nil if it cannot be retrieved + */ ++ (nullable NSString *)uidWithAccessibilityElement:(id)element; + +/** + Gets the unique identifier of the particular XCAccessibilityElement instance. + + @param element accessiblity element instance + @return the unique element identifier or nil if it cannot be retrieved + */ ++ (unsigned long long)idWithAccessibilityElement:(id)element; + +/** + Retrieves the list of required instance methods of the given protocol + + @param protocol target protocol reference + @return set of selector names + */ ++ (NSSet *)selectorNamesWithProtocol:(Protocol *)protocol; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBElementUtils.m b/WebDriverAgentLib/Routing/FBElementUtils.m new file mode 100644 index 0000000..cc89d2f --- /dev/null +++ b/WebDriverAgentLib/Routing/FBElementUtils.m @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBXCAccessibilityElement.h" +#import "FBElementUtils.h" +#import "FBElementTypeTransformer.h" + +NSString *const FBUnknownAttributeException = @"FBUnknownAttributeException"; +static NSString *const WD_PREFIX = @"wd"; +static NSString *const OBJC_PROP_GETTER_PREFIX = @"G"; +static NSString *const OBJC_PROP_ATTRIBS_SEPARATOR = @","; + +@implementation FBElementUtils + ++ (NSSet *)selectorNamesWithProtocol:(Protocol *)protocol +{ + unsigned int count; + struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, YES, YES, &count); + NSMutableSet *result = [NSMutableSet set]; + for (unsigned int i = 0; i < count; i++) { + SEL sel = methods[i].name; + if (nil != sel) { + [result addObject:NSStringFromSelector(sel)]; + } + } + free(methods); + return result.copy; +} + ++ (NSString *)wdAttributeNameForAttributeName:(NSString *)name +{ + NSAssert(name.length > 0, @"Attribute name cannot be empty", nil); + NSDictionary *attributeNamesMapping = [self.class wdAttributeNamesMapping]; + NSString *result = attributeNamesMapping[name]; + if (nil == result) { + NSString *description = [NSString stringWithFormat:@"The attribute '%@' is unknown. Valid attribute names are: %@", name, [attributeNamesMapping.allKeys sortedArrayUsingSelector:@selector(compare:)]]; + @throw [NSException exceptionWithName:FBUnknownAttributeException reason:description userInfo:@{}]; + return nil; + } + return result; +} + ++ (NSSet *)uniqueElementTypesWithElements:(NSArray> *)elements +{ + NSMutableSet *matchingTypes = [NSMutableSet set]; + [elements enumerateObjectsUsingBlock:^(id element, NSUInteger elementIdx, BOOL *stopElementsEnum) { + [matchingTypes addObject: @([FBElementTypeTransformer elementTypeWithTypeName:element.wdType])]; + }]; + return matchingTypes.copy; +} + ++ (NSDictionary *)wdAttributeNamesMapping +{ + static NSDictionary *attributeNamesMapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *wdPropertyGettersMapping = [NSMutableDictionary new]; + unsigned int propsCount = 0; + Protocol * aProtocol = objc_getProtocol(protocol_getName(@protocol(FBElement))); + objc_property_t *properties = protocol_copyPropertyList(aProtocol, &propsCount); + for (unsigned int i = 0; i < propsCount; ++i) { + objc_property_t property = properties[i]; + const char *name = property_getName(property); + NSString *nsName = [NSString stringWithUTF8String:name]; + if (nil == nsName || ![nsName hasPrefix:WD_PREFIX]) { + continue; + } + [wdPropertyGettersMapping setObject:[NSNull null] forKey:nsName]; + const char *c_attributes = property_getAttributes(property); + NSString *attributes = [NSString stringWithUTF8String:c_attributes]; + if (nil == attributes) { + continue; + } + // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html + NSArray *splitAttrs = [attributes componentsSeparatedByString:OBJC_PROP_ATTRIBS_SEPARATOR]; + for (NSString *part in splitAttrs) { + if ([part hasPrefix:OBJC_PROP_GETTER_PREFIX]) { + [wdPropertyGettersMapping setObject:[part substringFromIndex:1] forKey:nsName]; + break; + } + } + } + free(properties); + + NSMutableDictionary *resultCache = [NSMutableDictionary new]; + for (NSString *propName in wdPropertyGettersMapping) { + if ([[wdPropertyGettersMapping valueForKey:propName] isKindOfClass:NSNull.class]) { + // no getter + [resultCache setValue:propName forKey:propName]; + } else { + // has getter method + [resultCache setValue:[wdPropertyGettersMapping objectForKey:propName] forKey:propName]; + } + NSString *aliasName; + if (propName.length <= WD_PREFIX.length + 1) { + aliasName = [NSString stringWithFormat:@"%@", + [propName substringWithRange:NSMakeRange(WD_PREFIX.length, 1)].lowercaseString]; + } else { + NSString *propNameWithoutPrefix = [propName substringFromIndex:WD_PREFIX.length]; + NSString *firstPropNameCharacter = [propNameWithoutPrefix substringWithRange:NSMakeRange(0, 1)]; + if (![propNameWithoutPrefix isEqualToString:[propNameWithoutPrefix uppercaseString]]) { + // Lowercase the first character for the alias if the property name is not an uppercase abbreviation + firstPropNameCharacter = firstPropNameCharacter.lowercaseString; + } + aliasName = [NSString stringWithFormat:@"%@%@", firstPropNameCharacter, [propNameWithoutPrefix substringFromIndex:1]]; + } + if ([[wdPropertyGettersMapping valueForKey:propName] isKindOfClass:NSNull.class]) { + // no getter + [resultCache setValue:propName forKey:aliasName]; + } else { + // has getter method + [resultCache setValue:[wdPropertyGettersMapping objectForKey:propName] forKey:aliasName]; + } + } + attributeNamesMapping = resultCache.copy; + }); + return attributeNamesMapping; +} + ++ (NSString *)uidWithAccessibilityElement:(id)element +{ + unsigned long long elementId = [self.class idWithAccessibilityElement:element]; + int processId = element.processIdentifier; + if (elementId < 1 || processId < 1) { + return nil; + } + uint8_t b[16] = {0}; + memcpy(b, &elementId, sizeof(long long)); + memcpy(b + sizeof(long long), &processId, sizeof(int)); + NSUUID *uuidValue = [[NSUUID alloc] initWithUUIDBytes:b]; + return uuidValue.UUIDString; +} + +static BOOL FBShouldUsePayloadForUIDExtraction = YES; +static dispatch_once_t oncePayloadToken; ++ (unsigned long long)idWithAccessibilityElement:(id)element +{ + dispatch_once(&oncePayloadToken, ^{ + FBShouldUsePayloadForUIDExtraction = [(NSObject *)element respondsToSelector:@selector(payload)]; + }); + return FBShouldUsePayloadForUIDExtraction + ? [[element.payload objectForKey:@"uid.elementID"] longLongValue] + : [[(NSObject *)element valueForKey:@"_elementID"] longLongValue]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBExceptionHandler.h b/WebDriverAgentLib/Routing/FBExceptionHandler.h new file mode 100644 index 0000000..cc1dc0f --- /dev/null +++ b/WebDriverAgentLib/Routing/FBExceptionHandler.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Class used to handle exceptions raised by command handlers + */ +@interface FBExceptionHandler : NSObject + +/** + Handles 'exception' for 'webServer' raised while handling 'response' + + @param exception exception that needs handling + @param response response related to that exception + */ +- (void)handleException:(NSException *)exception forResponse:(RouteResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBExceptionHandler.m b/WebDriverAgentLib/Routing/FBExceptionHandler.m new file mode 100644 index 0000000..b5e1ec0 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBExceptionHandler.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBExceptionHandler.h" + +#import "RouteResponse.h" + +#import "FBResponsePayload.h" +#import "FBExceptions.h" + +@implementation FBExceptionHandler + +- (void)handleException:(NSException *)exception forResponse:(RouteResponse *)response +{ + FBCommandStatus *commandStatus; + NSString *traceback = [NSString stringWithFormat:@"%@", exception.callStackSymbols]; + if ([exception.name isEqualToString:FBSessionDoesNotExistException]) { + commandStatus = [FBCommandStatus noSuchDriverErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBInvalidArgumentException] + || [exception.name isEqualToString:FBElementAttributeUnknownException] + || [exception.name isEqualToString:FBApplicationMissingException]) { + commandStatus = [FBCommandStatus invalidArgumentErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBApplicationCrashedException] + || [exception.name isEqualToString:FBApplicationDeadlockDetectedException]) { + commandStatus = [FBCommandStatus invalidElementStateErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBInvalidXPathException] + || [exception.name isEqualToString:FBClassChainQueryParseException]) { + commandStatus = [FBCommandStatus invalidSelectorErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBElementNotVisibleException]) { + commandStatus = [FBCommandStatus elementNotVisibleErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBStaleElementException]) { + commandStatus = [FBCommandStatus staleElementReferenceErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBTimeoutException]) { + commandStatus = [FBCommandStatus timeoutErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBSessionCreationException]) { + commandStatus = [FBCommandStatus sessionNotCreatedError:exception.reason + traceback:traceback]; + } else { + commandStatus = [FBCommandStatus unknownErrorWithMessage:exception.reason + traceback:traceback]; + } + id payload = FBResponseWithStatus(commandStatus); + [payload dispatchWithResponse:response]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBExceptions.h b/WebDriverAgentLib/Routing/FBExceptions.h new file mode 100644 index 0000000..b802da6 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBExceptions.h @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/*! Exception used to notify about missing session */ +extern NSString *const FBSessionDoesNotExistException; + +/*! Exception used to notify about session creation issues */ +extern NSString *const FBSessionCreationException; + +/*! Exception used to notify about application deadlock */ +extern NSString *const FBApplicationDeadlockDetectedException; + +/*! Exception used to notify about unknown attribute */ +extern NSString *const FBElementAttributeUnknownException; + +/*! Exception used to notify about invalid argument */ +extern NSString *const FBInvalidArgumentException; + +/*! Exception used to notify about invisibility of an element while trying to interact with it */ +extern NSString *const FBElementNotVisibleException; + +/*! Exception used to notify about a timeout */ +extern NSString *const FBTimeoutException; + +/** + The exception happends if the cached element does not exist in DOM anymore + */ +extern NSString *const FBStaleElementException; + +/** + The exception happends if the provided XPath expession cannot be compiled because of a syntax error + */ +extern NSString *const FBInvalidXPathException; +/** + The exception happends if any internal error is triggered during XPath matching procedure + */ +extern NSString *const FBXPathQueryEvaluationException; + +/*! Exception used to notify about invalid class chain query */ +extern NSString *const FBClassChainQueryParseException; + +/*! Exception used to notify about application crash */ +extern NSString *const FBApplicationCrashedException; + +/*! Exception used to notify about the application is not installed */ +extern NSString *const FBApplicationMissingException; + +/*! Exception used to notify about WDA incompatibility with the current platform version */ +extern NSString *const FBIncompatibleWdaException; + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBExceptions.m b/WebDriverAgentLib/Routing/FBExceptions.m new file mode 100644 index 0000000..feb8a4d --- /dev/null +++ b/WebDriverAgentLib/Routing/FBExceptions.m @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBExceptions.h" + +NSString *const FBInvalidArgumentException = @"FBInvalidArgumentException"; +NSString *const FBSessionCreationException = @"FBSessionCreationException"; +NSString *const FBSessionDoesNotExistException = @"FBSessionDoesNotExistException"; +NSString *const FBApplicationDeadlockDetectedException = @"FBApplicationDeadlockDetectedException"; +NSString *const FBElementAttributeUnknownException = @"FBElementAttributeUnknownException"; +NSString *const FBElementNotVisibleException = @"FBElementNotVisibleException"; +NSString *const FBTimeoutException = @"FBTimeoutException"; +NSString *const FBStaleElementException = @"FBStaleElementException"; +NSString *const FBInvalidXPathException = @"FBInvalidXPathException"; +NSString *const FBXPathQueryEvaluationException = @"FBXPathQueryEvaluationException"; +NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseException"; +NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException"; +NSString *const FBApplicationMissingException = @"FBApplicationMissingException"; +NSString *const FBIncompatibleWdaException = @"FBIncompatibleWdaException"; diff --git a/WebDriverAgentLib/Routing/FBHTTPStatusCodes.h b/WebDriverAgentLib/Routing/FBHTTPStatusCodes.h new file mode 100644 index 0000000..a72590b --- /dev/null +++ b/WebDriverAgentLib/Routing/FBHTTPStatusCodes.h @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2013 Neo Visionaries Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef FBHTTPStatusCodes_h +#define FBHTTPStatusCodes_h + + +//---------------------------------------------------------------------- +// Typedef +//---------------------------------------------------------------------- + +/** + * HTTP status codes. + * + * The list here is based on the description at Wikipedia. + * The initial version of this list was written on April 20, 2013. + * + * @see List of HTTP status codes + */ +typedef enum +{ + /*-------------------------------------------------- + * 1xx Informational + *------------------------------------------------*/ + + /** + * 100 Continue. + */ + kHTTPStatusCodeContinue = 100, + + /** + * 101 Switching Protocols. + */ + kHTTPStatusCodeSwitchingProtocols = 101, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2518) + /** + * 103 Processing (WebDAV; RFC 2518). + */ + kHTTPStatusCodeProcessing = 102, +#endif + + /*-------------------------------------------------- + * 2xx Success + *------------------------------------------------*/ + + /** + * 200 OK. + */ + kHTTPStatusCodeOK = 200, + + /** + * 201 Created. + */ + kHTTPStatusCodeCreated = 201, + + /** + * 202 Accepted. + */ + kHTTPStatusCodeAccepted = 202, + + /** + * 203 Non-Authoritative Information (since HTTP/1.1). + */ + kHTTPStatusCodeNonAuthoritativeInformation = 203, + + /** + * 204 No Content. + */ + kHTTPStatusCodeNoContent = 204, + + /** + * 205 Reset Content. + */ + kHTTPStatusCodeResetContent = 205, + + /** + * 206 Partial Content. + */ + kHTTPStatusCodePartialContent = 206, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_4918) + /** + * 207 Multi-Status (WebDAV; RFC 4918). + */ + kHTTPStatusCodeMultiStatus = 207, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_5842) + /** + * 208 Already Reported (WebDAV; RFC 5842). + */ + kHTTPStatusCodeAlreadyReported = 208, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_3229) + /** + * 226 IM Used (RFC 3229) + */ + kHTTPStatusCodeIMUsed = 226, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2326) + /** + * 250 Low on Storage Space (RTSP; RFC 2326). + */ + kHTTPStatusCodeLowOnStorageSpace = 250, +#endif + + /*-------------------------------------------------- + * 3xx Redirection + *------------------------------------------------*/ + + /** + * 300 Multiple Choices. + */ + kHTTPStatusCodeMultipleChoices = 300, + + /** + * 301 Moved Permanently. + */ + kHTTPStatusCodeMovedPermanently = 301, + + /** + * 302 Found. + */ + kHTTPStatusCodeFound = 302, + + /** + * 303 See Other (since HTTP/1.1). + */ + kHTTPStatusCodeSeeOther = 303, + + /** + * 304 Not Modified. + */ + kHTTPStatusCodeNotModified = 304, + + /** + * 305 Use Proxy (since HTTP/1.1). + */ + kHTTPStatusCodeUseProxy = 305, + + /** + * 306 Switch Proxy. + */ + kHTTPStatusCodeSwitchProxy = 306, + + /** + * 307 Temporary Redirect (since HTTP/1.1). + */ + kHTTPStatusCodeTemporaryRedirect = 307, + + /** + * 308 Permanent Redirect (approved as experimental RFC). + */ + kHTTPStatusCodePermanentRedirect = 308, + + /*-------------------------------------------------- + * 4xx Client Error + *------------------------------------------------*/ + + /** + * 400 Bad Request. + */ + kHTTPStatusCodeBadRequest = 400, + + /** + * 401 Unauthorized. + */ + kHTTPStatusCodeUnauthorized = 401, + + /** + * 402 Payment Required. + */ + kHTTPStatusCodePaymentRequired = 402, + + /** + * 403 Forbidden. + */ + kHTTPStatusCodeForbidden = 403, + + /** + * 404 Not Found. + */ + kHTTPStatusCodeNotFound = 404, + + /** + * 405 Method Not Allowed. + */ + kHTTPStatusCodeMethodNotAllowed = 405, + + /** + * 406 Not Acceptable. + */ + kHTTPStatusCodeNotAcceptable = 406, + + /** + * 407 Proxy Authentication Required. + */ + kHTTPStatusCodeProxyAuthenticationRequired = 407, + + /** + * 408 Request Timeout. + */ + kHTTPStatusCodeRequestTimeout = 408, + + /** + * 409 Conflict. + */ + kHTTPStatusCodeConflict = 409, + + /** + * 410 Gone. + */ + kHTTPStatusCodeGone = 410, + + /** + * 411 Length Required. + */ + kHTTPStatusCodeLengthRequired = 411, + + /** + * 412 Precondition Failed. + */ + kHTTPStatusCodePreconditionFailed = 412, + + /** + * 413 Request Entity Too Large. + */ + kHTTPStatusCodeRequestEntityTooLarge = 413, + + /** + * 414 Request-URI Too Long. + */ + kHTTPStatusCodeRequestURITooLong = 414, + + /** + * 415 Unsupported Media Type. + */ + kHTTPStatusCodeUnsupportedMediaType = 415, + + /** + * 416 Requested Range Not Satisfiable. + */ + kHTTPStatusCodeRequestedRangeNotSatisfiable = 416, + + /** + * 417 Expectation Failed. + */ + kHTTPStatusCodeExpectationFailed = 417, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2324) + /** + * 418 I'm a teapot (RFC 2324). + */ + kHTTPStatusCodeImATeapot = 418, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_TWITTER) + /** + * 420 Enhance Your Calm (Twitter). + */ + kHTTPStatusCodeEnhanceYourCalm = 420, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_4918) + /** + * 422 Unprocessable Entity (WebDAV; RFC 4918). + */ + kHTTPStatusCodeUnprocessableEntity = 422, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_4918) + /** + * 423 Locked (WebDAV; RFC 4918). + */ + kHTTPStatusCodeLocked = 423, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_4918) + /** + * 424 Failed Dependency (WebDAV; RFC 4918). + */ + kHTTPStatusCodeFailedDependency = 424, +#endif + + /** + * 425 Unordered Collection (Internet draft). + */ + kHTTPStatusCodeUnorderedCollection = 425, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2817) + /** + * 426 Upgrade Required (RFC 2817). + */ + kHTTPStatusCodeUpgradeRequired = 426, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_6585) + /** + * 428 Precondition Required (RFC 6585). + */ + kHTTPStatusCodePreconditionRequired = 428, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_6585) + /** + * 429 Too Many Requests (RFC 6585). + */ + kHTTPStatusCodeTooManyRequests = 429, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_6585) + /** + * 431 Request Header Fields Too Large (RFC 6585). + */ + kHTTPStatusCodeRequestHeaderFieldsTooLarge = 431, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 444 No Response (Nginx). + */ + kHTTPStatusCodeNoResponse = 444, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_MICROSOFT) + /** + * 449 Retry With (Microsoft). + */ + kHTTPStatusCodeRetryWith = 449, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_MICROSOFT) + /** + * 450 Blocked by Windows Parental Controls (Microsoft). + */ + kHTTPStatusCodeBlockedByWindowsParentalControls = 450, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 451 Parameter Not Understood (RTSP). + */ + kHTTPStatusCodeParameterNotUnderstood = 451, +#endif + + /** + * 451 Unavailable For Legal Reasons (Internet draft). + */ + kHTTPStatusCodeUnavailableForLegalReasons = 451, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_MICROSOFT) + /** + * 451 Redirect (Microsoft). + */ + kHTTPStatusCodeRedirect = 451, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 452 Conference Not Found (RTSP). + */ + kHTTPStatusCodeConferenceNotFound = 452, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 453 Not Enough Bandwidth (RTSP). + */ + kHTTPStatusCodeNotEnoughBandwidth = 453, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 454 Session Not Found (RTSP). + */ + kHTTPStatusCodeSessionNotFound = 454, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 455 Method Not Valid in This State (RTSP). + */ + kHTTPStatusCodeMethodNotValidInThisState = 455, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 456 Header Field Not Valid for Resource (RTSP). + */ + kHTTPStatusCodeHeaderFieldNotValidForResource = 456, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 457 Invalid Range (RTSP). + */ + kHTTPStatusCodeInvalidRange = 457, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 458 Parameter Is Read-Only (RTSP). + */ + kHTTPStatusCodeParameterIsReadOnly = 458, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 459 Aggregate Operation Not Allowed (RTSP). + */ + kHTTPStatusCodeAggregateOperationNotAllowed = 459, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 460 Only Aggregate Operation Allowed (RTSP). + */ + kHTTPStatusCodeOnlyAggregateOperationAllowed = 460, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 461 Unsupported Transport (RTSP). + */ + kHTTPStatusCodeUnsupportedTransport = 461, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 462 Destination Unreachable (RTSP). + */ + kHTTPStatusCodeDestinationUnreachable = 462, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 494 Request Header Too Large (Nginx). + */ + kHTTPStatusCodeRequestHeaderTooLarge = 494, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 495 Cert Error (Nginx). + */ + kHTTPStatusCodeCertError = 495, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 496 No Cert (Nginx). + */ + kHTTPStatusCodeNoCert = 496, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 497 HTTP to HTTPS (Nginx). + */ + kHTTPStatusCodeHTTPToHTTPS = 497, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_NGINX) + /** + * 499 Client Closed Request (Nginx). + */ + kHTTPStatusCodeClientClosedRequest = 499, +#endif + + /*-------------------------------------------------- + * 5xx Server Error + *------------------------------------------------*/ + + /** + * 500 Internal Server Error. + */ + kHTTPStatusCodeInternalServerError = 500, + + /** + * 501 Not Implemented + */ + kHTTPStatusCodeNotImplemented = 501, + + /** + * 502 Bad Gateway. + */ + kHTTPStatusCodeBadGateway = 502, + + /** + * 503 Service Unavailable. + */ + kHTTPStatusCodeServiceUnavailable = 503, + + /** + * 504 Gateway Timeout. + */ + kHTTPStatusCodeGatewayTimeout = 504, + + /** + * 505 HTTP Version Not Supported. + */ + kHTTPStatusCodeHTTPVersionNotSupported = 505, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2295) + /** + * 506 Variant Also Negotiates (RFC 2295). + */ + kHTTPStatusCodeVariantAlsoNegotiates = 506, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_4918) + /** + * 507 Insufficient Storage (WebDAV; RFC 4918). + */ + kHTTPStatusCodeInsufficientStorage = 507, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_WEBDAV) && !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_5842) + /** + * 508 Loop Detected (WebDAV; RFC 5842). + */ + kHTTPStatusCodeLoopDetected = 508, +#endif + + /** + * 509 Bandwidth Limit Exceeded (Apache bw/limited extension). + */ + kHTTPStatusCodeBandwidthLimitExceeded = 509, + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_2774) + /** + * 510 Not Extended (RFC 2774). + */ + kHTTPStatusCodeNotExtended = 510, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RFC_6585) + /** + * 511 Network Authentication Required (RFC 6585). + */ + kHTTPStatusCodeNetworkAuthenticationRequired = 511, +#endif + +#if !defined(HTTP_STATUS_CODES_EXCLUDE_RTSP) + /** + * 551 Option not supported (RTSP). + */ + kHTTPStatusCodeOptionNotSupported = 551, +#endif + + /** + * 598 Network read timeout error (Unknown). + */ + kHTTPStatusCodeNetworkReadTimeoutError = 598, + + /** + * 599 Network connect timeout error (Unknown). + */ + kHTTPStatusCodeNetworkConnectTimeoutError = 599 +} +HTTPStatusCode; + + +#endif diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.h b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h new file mode 100644 index 0000000..8c863e9 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Class that represents WebDriverAgent JSON repsonse + */ +@interface FBResponseJSONPayload : NSObject + +/** + Initializer for JSON respond that converts given 'dictionary' to JSON + */ +- (instancetype)initWithDictionary:(NSDictionary *)dictionary + httpStatusCode:(HTTPStatusCode)httpStatusCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m new file mode 100644 index 0000000..8782b8e --- /dev/null +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBResponseJSONPayload.h" + +#import "FBLogger.h" +#import "NSDictionary+FBUtf8SafeDictionary.h" +#import "RouteResponse.h" + +@interface FBResponseJSONPayload () + +@property (nonatomic, copy, readonly) NSDictionary *dictionary; +@property (nonatomic, readonly) HTTPStatusCode httpStatusCode; + +@end + +@implementation FBResponseJSONPayload + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary + httpStatusCode:(HTTPStatusCode)httpStatusCode +{ + NSParameterAssert(dictionary); + if (!dictionary) { + return nil; + } + + self = [super init]; + if (self) { + _dictionary = dictionary; + _httpStatusCode = httpStatusCode; + } + return self; +} + +- (void)dispatchWithResponse:(RouteResponse *)response +{ + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self.dictionary + options:NSJSONWritingPrettyPrinted + error:&error]; + NSCAssert(jsonData, @"Valid JSON must be responded, error of %@", error); + if (nil == [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]) { + [FBLogger log:@"The incoming data cannot be encoded to UTF-8 JSON. Applying lossy conversion as a workaround."]; + jsonData = [NSJSONSerialization dataWithJSONObject:[self.dictionary fb_utf8SafeDictionary] + options:NSJSONWritingPrettyPrinted + error:&error]; + } + NSCAssert(jsonData, @"Valid JSON must be responded, error of %@", error); + [response setHeader:@"Content-Type" value:@"application/json;charset=UTF-8"]; + [response setStatusCode:self.httpStatusCode]; + [response respondWithData:jsonData]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBResponsePayload.h b/WebDriverAgentLib/Routing/FBResponsePayload.h new file mode 100644 index 0000000..b29b8f9 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBResponsePayload.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class FBElementCache; +@class RouteResponse; +@class XCUIElement; +@protocol FBResponsePayload; + +NS_ASSUME_NONNULL_BEGIN + +/** + Returns 'FBCommandStatusNoError' response payload + */ +id FBResponseWithOK(void); + +/** + Returns 'FBCommandStatusNoError' response payload with given 'object' + */ +id FBResponseWithObject(id _Nullable object); + +/** + Returns 'FBCommandStatusNoError' response payload with given 'element', which will be also cached in 'elementCache' + */ +id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact); + +/** + Returns 'FBCommandStatusNoError' response payload with given array of 'elements', which will be also cached in 'elementCache' + */ +id FBResponseWithCachedElements(NSArray *elements, FBElementCache *elementCache, BOOL compact); + +/** + Returns 'FBCommandStatusUnhandled' response payload with given error's description + */ +id FBResponseWithUnknownError(NSError *error); + +/** + Returns 'FBCommandStatusUnhandled' response payload with given error message + */ +id FBResponseWithUnknownErrorFormat(NSString *errorFormat, ...) NS_FORMAT_FUNCTION(1,2); + +/** + Returns 'status' response payload with given object + */ +id FBResponseWithStatus(FBCommandStatus *status); + +/** + Returns a response payload as a NSDictionary for given element. + Set compact=NO to include further attributes (defined by FBConfiguration.elementResponseAttributes) + */ +NSDictionary *FBDictionaryResponseWithElement(XCUIElement *element, BOOL compact); + + +/** + Protocol for objects that can dispatch some kind of a payload for given 'response' + */ +@protocol FBResponsePayload + +/** + Dispatch constructed payload into given response + */ +- (void)dispatchWithResponse:(RouteResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBResponsePayload.m b/WebDriverAgentLib/Routing/FBResponsePayload.m new file mode 100644 index 0000000..809cd41 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBResponsePayload.m @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBResponsePayload.h" + +#import "FBElementCache.h" +#import "FBResponseJSONPayload.h" +#import "FBSession.h" +#import "FBMathUtils.h" +#import "FBConfiguration.h" +#import "FBMacros.h" +#import "FBProtocolHelpers.h" +#import "XCUIElementQuery.h" +#import "XCUIElement+FBResolve.h" +#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" + +NSString *arbitraryAttrPrefix = @"attribute/"; + +id FBResponseWithOK(void) +{ + return FBResponseWithStatus(FBCommandStatus.ok); +} + +id FBResponseWithObject(id object) +{ + return FBResponseWithStatus([FBCommandStatus okWithValue:object]); +} + +XCUIElement *maybeStable(XCUIElement *element) +{ + BOOL useNativeCachingStrategy = nil == FBSession.activeSession + ? YES + : FBSession.activeSession.useNativeCachingStrategy; + if (useNativeCachingStrategy) { + return element; + } + + XCUIElement *result = element; + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_standardSnapshot]; + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + result = [element fb_stableInstanceWithUid:uid]; + } + return result; +} + +id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact) +{ + [elementCache storeElement:maybeStable(element)]; + NSDictionary *response = FBDictionaryResponseWithElement(element, compact); + element.lastSnapshot = nil; + return FBResponseWithStatus([FBCommandStatus okWithValue:response]); +} + +id FBResponseWithCachedElements(NSArray *elements, FBElementCache *elementCache, BOOL compact) +{ + NSMutableArray *elementsResponse = [NSMutableArray array]; + for (XCUIElement *element in elements) { + [elementCache storeElement:maybeStable(element)]; + [elementsResponse addObject:FBDictionaryResponseWithElement(element, compact)]; + element.lastSnapshot = nil; + } + return FBResponseWithStatus([FBCommandStatus okWithValue:elementsResponse]); +} + +id FBResponseWithUnknownError(NSError *error) +{ + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description traceback:nil]); +} + +id FBResponseWithUnknownErrorFormat(NSString *format, ...) +{ + va_list argList; + va_start(argList, format); + NSString *errorMessage = [[NSString alloc] initWithFormat:format arguments:argList]; + id payload = FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:errorMessage + traceback:nil]); + va_end(argList); + return payload; +} + +id FBResponseWithStatus(FBCommandStatus *status) +{ + NSMutableDictionary* response = [NSMutableDictionary dictionary]; + response[@"sessionId"] = [FBSession activeSession].identifier ?: NSNull.null; + if (nil == status.error) { + response[@"value"] = status.value ?: NSNull.null; + } else { + response[@"value"] = @{ + @"error": (id)status.error, + @"message": status.message ?: @"", + @"traceback": status.traceback ?: @"" + }; + } + return [[FBResponseJSONPayload alloc] initWithDictionary:response.copy + httpStatusCode:status.statusCode]; +} + +inline NSDictionary *FBDictionaryResponseWithElement(XCUIElement *element, BOOL compact) +{ + __block NSDictionary *elementResponse = nil; + @autoreleasepool { + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_customSnapshot]; + NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); + if (compact) { + elementResponse = compactResult; + return elementResponse; + } + + NSMutableDictionary *result = compactResult.mutableCopy; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; + for (NSString *field in fields) { + // 'name' here is the w3c-approved identifier for what we mean by 'type' + if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { + result[field] = wrappedSnapshot.wdType; + } else if ([field isEqualToString:@"text"]) { + result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; + } else if ([field isEqualToString:@"rect"]) { + result[field] = wrappedSnapshot.wdRect; + } else if ([field isEqualToString:@"enabled"]) { + result[field] = @(wrappedSnapshot.wdEnabled); + } else if ([field isEqualToString:@"displayed"]) { + result[field] = @(wrappedSnapshot.wdVisible); + } else if ([field isEqualToString:@"selected"]) { + result[field] = @(wrappedSnapshot.wdSelected); + } else if ([field isEqualToString:@"label"]) { + result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; + } else if ([field hasPrefix:arbitraryAttrPrefix]) { + NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; + result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; + } + } + elementResponse = result.copy; + } + return elementResponse; +} diff --git a/WebDriverAgentLib/Routing/FBRoute.h b/WebDriverAgentLib/Routing/FBRoute.h new file mode 100644 index 0000000..fce8dd8 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBRoute.h @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol FBResponsePayload; +@class FBRouteRequest; +@class RouteResponse; + +NS_ASSUME_NONNULL_BEGIN + +typedef __nonnull id (^FBRouteSyncHandler)(FBRouteRequest *request); + +/** + Class that represents route + */ +@interface FBRoute : NSObject + +/*! Route's verb (eg. POST, GET, DELETE) */ +@property (nonatomic, copy, readonly) NSString *verb; + +/*! Route's path */ +@property (nonatomic, copy, readonly) NSString *path; + +/** + Convenience constructor for GET route with given pathPattern + */ ++ (instancetype)GET:(NSString *)pathPattern; + +/** + Convenience constructor for POST route with given pathPattern + */ ++ (instancetype)POST:(NSString *)pathPattern; + +/** + Convenience constructor for PUT route with given pathPattern + */ ++ (instancetype)PUT:(NSString *)pathPattern; + +/** + Convenience constructor for DELETE route with given pathPattern + */ ++ (instancetype)DELETE:(NSString *)pathPattern; + +/** + Convenience constructor for OPTIONS route with given pathPattern +*/ ++ (instancetype)OPTIONS:(NSString *)pathPattern; + +/** + Chain-able constructor that handles response with given FBRouteSyncHandler block + */ +- (instancetype)respondWithBlock:(FBRouteSyncHandler)handler; + +/** + Chain-able constructor that handles response with given FBRouteSyncHandler block + */ +- (instancetype)respondWithTarget:(id)target action:(SEL)selector; + +/** + Chain-able constructor for route that does NOT require session + */ +- (instancetype)withoutSession; + +/** + Dispatches response for request + */ +- (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBRoute.m b/WebDriverAgentLib/Routing/FBRoute.m new file mode 100644 index 0000000..fbe69b8 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBRoute.m @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBRoute.h" +#import "FBRouteRequest-Private.h" + +#import + +#import "FBExceptionHandler.h" +#import "FBExceptions.h" +#import "FBResponsePayload.h" +#import "FBSession.h" + +@interface FBRoute () +@property (nonatomic, assign, readwrite) BOOL requiresSession; +@property (nonatomic, copy, readwrite) NSString *verb; +@property (nonatomic, copy, readwrite) NSString *path; + +- (void)decorateRequest:(FBRouteRequest *)request; + +@end + +static NSString *const FBRouteSessionPrefix = @"/session/:sessionID"; + +@interface FBRoute_TargetAction : FBRoute +@property (nonatomic, strong, readwrite) id target; +@property (nonatomic, assign, readwrite) SEL action; +@end + + +@implementation FBRoute_TargetAction + +- (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response +{ + [self decorateRequest:request]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + id (*requestMsgSend)(id, SEL, FBRouteRequest *) = ((id(*)(id, SEL, FBRouteRequest *))objc_msgSend); +#pragma clang diagnostic pop + id payload = requestMsgSend(self.target, self.action, request); + [payload dispatchWithResponse:response]; +} + +@end + + +@interface FBRoute_Sync : FBRoute +@property (nonatomic, copy, readwrite) FBRouteSyncHandler handler; +@end + + +@implementation FBRoute_Sync + +- (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response +{ + [self decorateRequest:request]; + id payload = self.handler(request); + [payload dispatchWithResponse:response]; +} + +@end + + +@implementation FBRoute + ++ (instancetype)withVerb:(NSString *)verb path:(NSString *)pathPattern requiresSession:(BOOL)requiresSession +{ + FBRoute *route = [self new]; + route.verb = verb; + route.path = [FBRoute pathPatternWithSession:pathPattern requiresSession:requiresSession]; + route.requiresSession = requiresSession; + return route; +} + ++ (instancetype)OPTIONS:(NSString *)pathPattern +{ + return [self withVerb:@"OPTIONS" path:pathPattern requiresSession:NO]; +} + ++ (instancetype)GET:(NSString *)pathPattern +{ + return [self withVerb:@"GET" path:pathPattern requiresSession:YES]; +} + ++ (instancetype)POST:(NSString *)pathPattern +{ + return [self withVerb:@"POST" path:pathPattern requiresSession:YES]; +} + ++ (instancetype)PUT:(NSString *)pathPattern +{ + return [self withVerb:@"PUT" path:pathPattern requiresSession:YES]; +} + ++ (instancetype)DELETE:(NSString *)pathPattern +{ + return [self withVerb:@"DELETE" path:pathPattern requiresSession:YES]; +} + ++ (NSString *)pathPatternWithSession:(NSString *)pathPattern requiresSession:(BOOL)requiresSession +{ + NSRange range = [pathPattern rangeOfString:FBRouteSessionPrefix]; + if (requiresSession) { + if (range.location != 0) { + pathPattern = [FBRouteSessionPrefix stringByAppendingPathComponent:pathPattern]; + } + } else { + if (range.location == 0) { + pathPattern = [pathPattern stringByReplacingCharactersInRange:range withString:@""]; + } + } + if (pathPattern.length == 0) { + pathPattern = @"/"; + } + return pathPattern; +} + +- (instancetype)withoutSession +{ + self.requiresSession = NO; + return self; +} + +- (instancetype)respondWithBlock:(FBRouteSyncHandler)handler +{ + FBRoute_Sync *route = [FBRoute_Sync withVerb:self.verb path:self.path requiresSession:self.requiresSession]; + route.handler = handler; + return route; +} + +- (instancetype)respondWithTarget:(id)target action:(SEL)action +{ + FBRoute_TargetAction *route = [FBRoute_TargetAction withVerb:self.verb path:self.path requiresSession:self.requiresSession]; + route.target = target; + route.action = action; + return route; +} + +- (void)decorateRequest:(FBRouteRequest *)request +{ + if (!self.requiresSession) { + return; + } + NSString *sessionID = request.parameters[@"sessionID"]; + if (!sessionID) { + [self raiseNoSessionException]; + return; + } + FBSession *session = [FBSession sessionWithIdentifier:sessionID]; + if (!session) { + [self raiseNoSessionException]; + return; + } + request.session = session; +} + +- (void)raiseNoSessionException +{ + [[NSException exceptionWithName:FBSessionDoesNotExistException reason:@"Session does not exist" userInfo:nil] raise]; +} + +- (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response +{ + id payload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Unhandled route" + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + [payload dispatchWithResponse:response]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBRouteRequest-Private.h b/WebDriverAgentLib/Routing/FBRouteRequest-Private.h new file mode 100644 index 0000000..7144bd8 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBRouteRequest-Private.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBRouteRequest () +@property (nonatomic, strong, readwrite) NSURL *URL; +@property (nonatomic, copy, readwrite) NSDictionary *parameters; +@property (nonatomic, copy, readwrite) NSDictionary *arguments; +@property (nonatomic, strong, readwrite) FBSession *session; +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.h b/WebDriverAgentLib/Routing/FBRouteRequest.h new file mode 100644 index 0000000..c7938d5 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBRouteRequest.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class FBSession; + +NS_ASSUME_NONNULL_BEGIN + +/** + Class that represents WebDriverAgent command request + */ +@interface FBRouteRequest : NSObject + +/*! Request's URL */ +@property (nonatomic, strong, readonly) NSURL *URL; + +/*! Parameters sent with that request */ +@property (nonatomic, copy, readonly) NSDictionary *parameters; + +/*! Arguments sent with that request */ +@property (nonatomic, copy, readonly) NSDictionary *arguments; + +/*! Session associated with that request */ +@property (nonatomic, strong, readonly) FBSession *session; + +/** + Convenience constructor for request + */ ++ (instancetype)routeRequestWithURL:(NSURL *)URL parameters:(NSDictionary *)parameters arguments:(NSDictionary *)arguments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.m b/WebDriverAgentLib/Routing/FBRouteRequest.m new file mode 100644 index 0000000..b8656d2 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBRouteRequest.m @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBRouteRequest-Private.h" + +@implementation FBRouteRequest + ++ (instancetype)routeRequestWithURL:(NSURL *)URL parameters:(NSDictionary *)parameters arguments:(NSDictionary *)arguments +{ + FBRouteRequest *request = [self.class new]; + request.URL = URL; + request.parameters = parameters; + request.arguments = arguments; + return request; +} + +- (NSString *)description +{ + return [NSString stringWithFormat: + @"Request URL %@ | Params %@ | Arguments %@", + self.URL, + self.parameters, + self.arguments + ]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h new file mode 100644 index 0000000..ce655dd --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h @@ -0,0 +1,56 @@ +/** + * + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class FBScreenRecordingPromise; + +@interface FBScreenRecordingContainer : NSObject + +/** The amount of video FPS */ +@property (readonly, nonatomic) NSUInteger fps; +/** Codec to use, where 0 is h264, 1 - HEVC */ +@property (readonly, nonatomic) long long codec; +/** Keep the currently active screen resording promise. Equals to nil if no active screen recordings are running */ +@property (readonly, nonatomic, nullable) FBScreenRecordingPromise* screenRecordingPromise; +/** The timestamp of the video startup as Unix float seconds */ +@property (readonly, nonatomic, nullable) NSNumber *startedAt; + +/** +@return singleton instance + */ ++ (instancetype)sharedInstance; + +/** + Keeps current screen recording promise + + @param screenRecordingPromise a promise to set + @param fps FPS value + @param codec Codec value + */ +- (void)storeScreenRecordingPromise:(FBScreenRecordingPromise *)screenRecordingPromise + fps:(NSUInteger)fps + codec:(long long)codec; +/** + Resets the current screen recording promise + */ +- (void)reset; + +/** + Transforms the container content to a dictionary. + + @return May return nil if no screen recording is currently running + */ +- (nullable NSDictionary *)toDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m new file mode 100644 index 0000000..608d7bf --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m @@ -0,0 +1,72 @@ +/** + * + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreenRecordingContainer.h" + +#import "FBScreenRecordingPromise.h" + +@interface FBScreenRecordingContainer () + +@property (readwrite) NSUInteger fps; +@property (readwrite) long long codec; +@property (readwrite) FBScreenRecordingPromise* screenRecordingPromise; +@property (readwrite) NSNumber *startedAt; + +@end + +@implementation FBScreenRecordingContainer + ++ (instancetype)sharedInstance +{ + static FBScreenRecordingContainer *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (void)storeScreenRecordingPromise:(FBScreenRecordingPromise *)screenRecordingPromise + fps:(NSUInteger)fps + codec:(long long)codec; +{ + self.fps = fps; + self.codec = codec; + self.screenRecordingPromise = screenRecordingPromise; + self.startedAt = @([NSDate.date timeIntervalSince1970]); +} + +- (void)reset; +{ + self.fps = 0; + self.codec = 0; + if (nil != self.screenRecordingPromise) { + [XCTContext runActivityNamed:@"Video Cleanup" block:^(id activity){ + [activity addAttachment:(XCTAttachment *)self.screenRecordingPromise.nativePromise]; + }]; + self.screenRecordingPromise = nil; + } + self.startedAt = nil; +} + +- (nullable NSDictionary *)toDictionary +{ + if (nil == self.screenRecordingPromise) { + return nil; + } + + return @{ + @"fps": @(self.fps), + @"codec": @(self.codec), + @"uuid": [self.screenRecordingPromise identifier].UUIDString ?: [NSNull null], + @"startedAt": self.startedAt ?: [NSNull null], + }; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h new file mode 100644 index 0000000..6b3fcb3 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenRecordingPromise : NSObject + +/** Unique identiifier of the video recording, also used as the default file name */ +@property (nonatomic, readonly) NSUUID *identifier; +/** Native screen recording promise */ +@property (nonatomic, readonly) id nativePromise; + +/** + Creates a wrapper object for a native screen recording promise + + @param promise Native promise object to be wrapped + */ +- (instancetype)initWithNativePromise:(id)promise; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m new file mode 100644 index 0000000..e1193a1 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m @@ -0,0 +1,31 @@ +/** + * + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreenRecordingPromise.h" + +@interface FBScreenRecordingPromise () +@property (readwrite) id nativePromise; +@end + +@implementation FBScreenRecordingPromise + +- (instancetype)initWithNativePromise:(id)promise +{ + if ((self = [super init])) { + self.nativePromise = promise; + } + return self; +} + +- (NSUUID *)identifier +{ + return (NSUUID *)[self.nativePromise valueForKey:@"_UUID"]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h new file mode 100644 index 0000000..f0a61ef --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenRecordingRequest : NSObject + +/** The amount of video FPS */ +@property (readonly, nonatomic) NSUInteger fps; +/** Codec to use, where 0 is h264, 1 - HEVC */ +@property (readonly, nonatomic) long long codec; + +/** + Creates a custom wrapper for a screen recording reqeust + + @param fps FPS value, see baove + @param codec Codex value, see above + */ +- (instancetype)initWithFps:(NSUInteger)fps codec:(long long)codec; + +/** + Transforms the current wrapper instance to a native object, + which is ready to be passed to XCTest APIs + + @param error If there was a failure converting the instance to a native object + @returns Native object instance + */ +- (nullable id)toNativeRequestWithError:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m new file mode 100644 index 0000000..32c5b05 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreenRecordingRequest.h" + +#import "FBErrorBuilder.h" +#import "XCUIScreen.h" + +@implementation FBScreenRecordingRequest + +- (instancetype)initWithFps:(NSUInteger)fps codec:(long long)codec +{ + if ((self = [super init])) { + _fps = fps; + _codec = codec; + } + return self; +} + +- (nullable id)createVideoEncodingWithError:(NSError **)error +{ + Class videoEncodingClass = NSClassFromString(@"XCTVideoEncoding"); + if (nil == videoEncodingClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTVideoEncoding class"] + buildError:error]; + return nil; + } + + id videoEncodingAllocated = [videoEncodingClass alloc]; + SEL videoEncodingConstructorSelector = NSSelectorFromString(@"initWithCodec:frameRate:"); + if (![videoEncodingAllocated respondsToSelector:videoEncodingConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithCodec:frameRate:' contructor is not found on XCTVideoEncoding class"] + buildError:error]; + return nil; + } + + NSMethodSignature *videoEncodingContructorSignature = [videoEncodingAllocated methodSignatureForSelector:videoEncodingConstructorSelector]; + NSInvocation *videoEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:videoEncodingContructorSignature]; + [videoEncodingInitInvocation setSelector:videoEncodingConstructorSelector]; + long long codec = self.codec; + [videoEncodingInitInvocation setArgument:&codec atIndex:2]; + double frameRate = self.fps; + [videoEncodingInitInvocation setArgument:&frameRate atIndex:3]; + [videoEncodingInitInvocation invokeWithTarget:videoEncodingAllocated]; + id __unsafe_unretained result; + [videoEncodingInitInvocation getReturnValue:&result]; + return result; +} + +- (id)toNativeRequestWithError:(NSError **)error +{ + Class screenRecordingRequestClass = NSClassFromString(@"XCTScreenRecordingRequest"); + if (nil == screenRecordingRequestClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTScreenRecordingRequest class"] + buildError:error]; + return nil; + } + + id screenRecordingRequestAllocated = [screenRecordingRequestClass alloc]; + SEL screenRecordingRequestConstructorSelector = NSSelectorFromString(@"initWithScreenID:rect:preferredEncoding:"); + if (![screenRecordingRequestAllocated respondsToSelector:screenRecordingRequestConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithScreenID:rect:preferredEncoding:' contructor is not found on XCTScreenRecordingRequest class"] + buildError:error]; + return nil; + } + id videoEncoding = [self createVideoEncodingWithError:error]; + if (nil == videoEncoding) { + return nil; + } + + NSMethodSignature *screenRecordingRequestContructorSignature = [screenRecordingRequestAllocated methodSignatureForSelector:screenRecordingRequestConstructorSelector]; + NSInvocation *screenRecordingRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenRecordingRequestContructorSignature]; + [screenRecordingRequestInitInvocation setSelector:screenRecordingRequestConstructorSelector]; + long long mainScreenId = XCUIScreen.mainScreen.displayID; + [screenRecordingRequestInitInvocation setArgument:&mainScreenId atIndex:2]; + CGRect fullScreenRect = CGRectNull; + [screenRecordingRequestInitInvocation setArgument:&fullScreenRect atIndex:3]; + [screenRecordingRequestInitInvocation setArgument:&videoEncoding atIndex:4]; + [screenRecordingRequestInitInvocation invokeWithTarget:screenRecordingRequestAllocated]; + id __unsafe_unretained result; + [screenRecordingRequestInitInvocation getReturnValue:&result]; + return result; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBSession-Private.h b/WebDriverAgentLib/Routing/FBSession-Private.h new file mode 100644 index 0000000..792fcd7 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBSession-Private.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class FBElementCache; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBSession () +@property (nonatomic, copy, readwrite) NSString *identifier; +@property (nonatomic, strong, readwrite) FBElementCache *elementCache; + +/** + Sets session as current session + */ ++ (void)markSessionActive:(FBSession *)session; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBSession.h b/WebDriverAgentLib/Routing/FBSession.h new file mode 100644 index 0000000..61b1f87 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBSession.h @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class FBElementCache; +@class XCUIApplication; + +NS_ASSUME_NONNULL_BEGIN + +/** Bundle identifier of Mobile Safari browser */ +extern NSString* const FB_SAFARI_BUNDLE_ID; + +/** + Class that represents testing session + */ +@interface FBSession : NSObject + +/*! Application tested during that session */ +@property (nonatomic, readonly) XCUIApplication *activeApplication; + +/*! Session's identifier */ +@property (nonatomic, readonly) NSString *identifier; + +/*! Element cache related to that session */ +@property (nonatomic, readonly) FBElementCache *elementCache; + +/*! The identifier of the active application */ +@property (nonatomic) NSString *defaultActiveApplication; + +/*! The action to apply to unexpected alerts. Either "accept"/"dismiss" or nil/empty string (by default) to do nothing */ +@property (nonatomic, nullable) NSString *defaultAlertAction; + +/*! Whether to use the native caching strategy for elements or the custom one: https://discuss.appium.io/t/elements-state-coming-from-xpath-vs-ios-predicate-string/34016 */ +@property (nonatomic) BOOL useNativeCachingStrategy; + +/*! Keeps cached visibility values for the current snapshots tree */ +@property (nonatomic, readonly) NSMutableDictionary *> *elementsVisibilityCache; + ++ (nullable instancetype)activeSession; + +/** + Fetches session for given identifier. + If identifier doesn't match activeSession identifier, will return nil. + + @param identifier Identifier for searched session + @return session. Can return nil if session does not exists + */ ++ (nullable instancetype)sessionWithIdentifier:(NSString *)identifier; + +/** + Creates and saves new session for application + + @param application The application that we want to create session for + @return new session + */ ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application; + +/** + Creates and saves new session for application with default alert handling behaviour + + @param application The application that we want to create session for + @param defaultAlertAction The default reaction to on-screen alert. Either 'accept' or 'dismiss' + @return new session + */ ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application + defaultAlertAction:(NSString *)defaultAlertAction; + +/** + Kills application associated with that session and removes session + */ +- (void)kill; + +/** + Launch an application with given bundle identifier in scope of current session. + !This method is only available since Xcode9 SDK + + @param bundleIdentifier Valid bundle identifier of the application to be launched + @param shouldWaitForQuiescence whether to wait for quiescence on application startup + @param arguments The optional array of application command line arguments. The arguments are going to be applied if the application was not running before. + @param environment The optional dictionary of environment variables for the application, which is going to be executed. The environment variables are going to be applied if the application was not running before. + @return The application instance + */ +- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier + shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence + arguments:(nullable NSArray *)arguments + environment:(nullable NSDictionary *)environment; + +/** + Activate an application with given bundle identifier in scope of current session. + !This method is only available since Xcode9 SDK + + @param bundleIdentifier Valid bundle identifier of the application to be activated + @return The application instance + */ +- (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier; + +/** + Terminate an application with the given bundle id. The application should be previously + executed by launchApplicationWithBundleId method or passed to the init method. + + @param bundleIdentifier Valid bundle identifier of the application to be terminated + @return Either YES if the app has been successfully terminated or NO if it was not running + */ +- (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier; + +/** + Get the state of the particular application in scope of the current session. + !This method is only returning reliable results since Xcode9 SDK + + @param bundleIdentifier Valid bundle identifier of the application to get the state from + @return Application state as integer number. See + https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc + for more details on possible enum values + */ +- (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier; + +/** + Allows to enable automated session alerts monitoring. + Repeated calls are ignored if alerts monitoring has been already enabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)enableAlertsMonitor; + +/** + Allows to disable automated alerts monitoring + Repeated calls are ignored if alerts monitoring has been already disabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)disableAlertsMonitor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgentLib/Routing/FBSession.m new file mode 100644 index 0000000..b8de5ed --- /dev/null +++ b/WebDriverAgentLib/Routing/FBSession.m @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBSession.h" +#import "FBSession-Private.h" + +#import + +#import "FBXCAccessibilityElement.h" +#import "FBAlertsMonitor.h" +#import "FBConfiguration.h" +#import "FBElementCache.h" +#import "FBExceptions.h" +#import "FBMacros.h" +#import "FBScreenRecordingContainer.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCUIApplication+FBQuiescence.h" +#import "XCUIElement.h" +#import "XCUIElement+FBClassChain.h" + +/*! + The intial value for the default application property. + Setting this value to `defaultActiveApplication` property forces WDA to use the internal + automated algorithm to determine the active on-screen application + */ +NSString *const FBDefaultApplicationAuto = @"auto"; + +NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari"; + +@interface FBSession () +@property (nullable, nonatomic) XCUIApplication *testedApplication; +@property (nonatomic) BOOL isTestedApplicationExpectedToRun; +@property (nonatomic) BOOL shouldAppsWaitForQuiescence; +@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor; +@property (nonatomic, readwrite) NSMutableDictionary *> *elementsVisibilityCache; +@end + +@interface FBSession (FBAlertsMonitorDelegate) + +- (void)didDetectAlert:(FBAlert *)alert; + +@end + +@implementation FBSession (FBAlertsMonitorDelegate) + +- (void)didDetectAlert:(FBAlert *)alert +{ + NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector; + if ([autoClickAlertSelector length] > 0) { + @try { + NSArray *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector + shouldReturnAfterFirstMatch:YES]; + if (matches.count > 0) { + [[matches objectAtIndex:0] tap]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@", + autoClickAlertSelector, e.description]; + } + // This setting has priority over other settings if enabled + return; + } + + if (nil == self.defaultAlertAction || 0 == self.defaultAlertAction.length) { + return; + } + + NSError *error; + if ([self.defaultAlertAction isEqualToString:@"accept"]) { + if (![alert acceptWithError:&error]) { + [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", error.description]; + } + } else if ([self.defaultAlertAction isEqualToString:@"dismiss"]) { + if (![alert dismissWithError:&error]) { + [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", error.description]; + } + } else { + [FBLogger logFmt:@"'%@' default alert action is unsupported", self.defaultAlertAction]; + } +} + +@end + +@implementation FBSession + +static FBSession *_activeSession = nil; + ++ (instancetype)activeSession +{ + return _activeSession; +} + ++ (void)markSessionActive:(FBSession *)session +{ + if (_activeSession) { + [_activeSession kill]; + } + _activeSession = session; +} + ++ (instancetype)sessionWithIdentifier:(NSString *)identifier +{ + if (!identifier) { + return nil; + } + if (![identifier isEqualToString:_activeSession.identifier]) { + return nil; + } + return _activeSession; +} + ++ (instancetype)initWithApplication:(XCUIApplication *)application +{ + FBSession *session = [FBSession new]; + session.useNativeCachingStrategy = YES; + session.alertsMonitor = nil; + session.defaultAlertAction = nil; + session.elementsVisibilityCache = [NSMutableDictionary dictionary]; + session.identifier = [[NSUUID UUID] UUIDString]; + session.defaultActiveApplication = FBDefaultApplicationAuto; + session.testedApplication = nil; + session.isTestedApplicationExpectedToRun = nil != application && application.running; + if (application) { + session.testedApplication = application; + session.shouldAppsWaitForQuiescence = application.fb_shouldWaitForQuiescence; + } + session.elementCache = [FBElementCache new]; + [FBSession markSessionActive:session]; + return session; +} + ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application + defaultAlertAction:(NSString *)defaultAlertAction +{ + FBSession *session = [self.class initWithApplication:application]; + session.defaultAlertAction = [defaultAlertAction lowercaseString]; + [session enableAlertsMonitor]; + return session; +} + +- (BOOL)enableAlertsMonitor +{ + if (nil != self.alertsMonitor) { + return NO; + } + + self.alertsMonitor = [[FBAlertsMonitor alloc] init]; + self.alertsMonitor.delegate = (id)self; + [self.alertsMonitor enable]; + return YES; +} + +- (BOOL)disableAlertsMonitor +{ + if (nil == self.alertsMonitor) { + return NO; + } + + [self.alertsMonitor disable]; + self.alertsMonitor = nil; + return YES; +} + +- (void)kill +{ + if (nil == _activeSession) { + return; + } + + [self disableAlertsMonitor]; + + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil != activeScreenRecording) { + NSError *error; + if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) { + [FBLogger logFmt:@"%@", error]; + } + [FBScreenRecordingContainer.sharedInstance reset]; + } + + if (nil != self.testedApplication + && FBConfiguration.shouldTerminateApp + && self.testedApplication.running + && ![self.testedApplication fb_isSameAppAs:XCUIApplication.fb_systemApplication]) { + @try { + [self.testedApplication terminate]; + } @catch (NSException *e) { + [FBLogger logFmt:@"%@", e.description]; + } + } + + _activeSession = nil; +} + +- (XCUIApplication *)activeApplication +{ + BOOL isAuto = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto]; + NSString *defaultBundleId = isAuto ? nil : self.defaultActiveApplication; + + if (nil != defaultBundleId && [self applicationStateWithBundleId:defaultBundleId] >= XCUIApplicationStateRunningForeground) { + return [self makeApplicationWithBundleId:defaultBundleId]; + } + + if (nil != self.testedApplication) { + XCUIApplicationState testedAppState = self.testedApplication.state; + if (testedAppState >= XCUIApplicationStateRunningForeground) { + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}", + @"elementType", @(XCUIElementTypeAlert), + // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946 + @"identifier", @"SBTransientOverlayWindow", + // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting + // See https://github.com/appium/appium/issues/20835 + @"NotificationShortLookView"]; + if ([FBConfiguration shouldRespectSystemAlerts] + && [[XCUIApplication.fb_systemApplication descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:searchPredicate].count > 0) { + return XCUIApplication.fb_systemApplication; + } + return (XCUIApplication *)self.testedApplication; + } + if (self.isTestedApplicationExpectedToRun && testedAppState <= XCUIApplicationStateNotRunning) { + NSString *description = [NSString stringWithFormat:@"The application under test with bundle id '%@' is not running, possibly crashed", self.testedApplication.bundleID]; + @throw [NSException exceptionWithName:FBApplicationCrashedException reason:description userInfo:nil]; + } + } + + return [XCUIApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId]; +} + +- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier + shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence + arguments:(nullable NSArray *)arguments + environment:(nullable NSDictionary *)environment +{ + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; + if (nil == shouldWaitForQuiescence) { + // Iherit the quiescence check setting from the main app under test by default + app.fb_shouldWaitForQuiescence = nil != self.testedApplication && self.shouldAppsWaitForQuiescence; + } else { + app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue]; + } + if (!app.running) { + app.launchArguments = arguments ?: @[]; + app.launchEnvironment = environment ?: @{}; + [app launch]; + } else { + [app activate]; + } + if ([app fb_isSameAppAs:self.testedApplication]) { + self.isTestedApplicationExpectedToRun = YES; + } + return app; +} + +- (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier +{ + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; + [app activate]; + return app; +} + +- (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier +{ + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; + if ([app fb_isSameAppAs:self.testedApplication]) { + self.isTestedApplicationExpectedToRun = NO; + } + if (app.running) { + [app terminate]; + return YES; + } + return NO; +} + +- (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier +{ + return [self makeApplicationWithBundleId:bundleIdentifier].state; +} + +- (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier +{ + return nil != self.testedApplication && [bundleIdentifier isEqualToString:(NSString *)self.testedApplication.bundleID] + ? self.testedApplication + : [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgentLib/Routing/FBTCPSocket.h new file mode 100644 index 0000000..31adc7e --- /dev/null +++ b/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "GCDAsyncSocket.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol FBTCPSocketDelegate + +/** + The callback which is fired on new TCP client connection + + @param newClient The newly connected socket + */ +- (void)didClientConnect:(GCDAsyncSocket *)newClient; + +/** + The callback which is fired when the TCP server receives a data from a connected client + + @param client The client, which sent the data +*/ +- (void)didClientSendData:(GCDAsyncSocket *)client; + +/** + The callback which is fired when TCP client disconnects + + @param client The actual diconnected client + */ +- (void)didClientDisconnect:(GCDAsyncSocket *)client; + +@end + + +@interface FBTCPSocket : NSObject + +@property (nullable, nonatomic) id delegate; + +/** + Creates TCP socket isntance which is going to be started on the specified port + + @param port The actual port number + @return self instance + */ +- (instancetype)initWithPort:(uint16_t)port; + +/** + Starts TCP socket listener on the specified port + + @param error The alias to the actual startup error descirption or nil if the socket has started and is listening + @return NO If there was an error + */ +- (BOOL)startWithError:(NSError **)error; + +/** + Stops the socket if it is running + */ +- (void)stop; +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m new file mode 100644 index 0000000..fabd220 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTCPSocket.h" + + +@interface FBTCPSocket() +@property (readonly, nonatomic) dispatch_queue_t socketQueue; +@property (readonly, nonatomic) GCDAsyncSocket *listeningSocket; +@property (readonly, nonatomic) NSMutableArray *connectedClients; +@property (readonly, nonatomic) uint16_t port; +@end + + +@interface FBTCPSocket(AsyncSocket) + +@end + + +@implementation FBTCPSocket + +- (instancetype)initWithPort:(uint16_t)port +{ + if ((self = [super init])) { + _socketQueue = dispatch_queue_create("socketQueue", NULL); + _listeningSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_socketQueue]; + _connectedClients = [[NSMutableArray alloc] initWithCapacity:1]; + _port = port; + _delegate = nil; + } + return self; +} + +- (BOOL)startWithError:(NSError **)error +{ + if (![self.listeningSocket acceptOnPort:self.port error:error]) { + return NO;; + } + + return YES; +} + +- (void)stop +{ + @synchronized(self.connectedClients) { + for (NSUInteger i = 0; i < [self.connectedClients count]; i++) { + [[self.connectedClients objectAtIndex:i] disconnect]; + } + } + + [self.listeningSocket disconnect]; +} + +@end + + +@implementation FBTCPSocket(AsyncSocket) + +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket +{ + @synchronized(self.connectedClients) { + [self.connectedClients addObject:newSocket]; + } + [self.delegate didClientConnect:newSocket]; +} + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + [self.delegate didClientSendData:sock]; +} + +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + @synchronized(self.connectedClients) { + [self.connectedClients removeObject:sock]; + } + [self.delegate didClientDisconnect:sock]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBWebServer.h b/WebDriverAgentLib/Routing/FBWebServer.h new file mode 100644 index 0000000..34bca65 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBWebServer.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class RouteResponse, RoutingHTTPServer, FBExceptionHandler; +@protocol FBWebServerDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + HTTP and USB service wrapper, handling requests and responses + */ +@interface FBWebServer : NSObject + +/** + Server delegate. + */ +@property (weak, nonatomic) id delegate; + +/** + Starts WebDriverAgent service by booting HTTP and USB server + */ +- (void)startServing; + +/** + Stops WebDriverAgent service, shutting down HTTP and USB servers. + */ +- (void)stopServing; + +@end + +/** + The protocol allowing the server delegate to handle messages from the server. + */ +@protocol FBWebServerDelegate + +/** + The server requested WebDriverAgent service shutdown. + + @param webServer Server instance. + */ +- (void)webServerDidRequestShutdown:(FBWebServer *)webServer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m new file mode 100644 index 0000000..62a71df --- /dev/null +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBWebServer.h" + +#import "RoutingConnection.h" +#import "RoutingHTTPServer.h" + +#import "FBCommandHandler.h" +#import "FBErrorBuilder.h" +#import "FBExceptionHandler.h" +#import "FBMjpegServer.h" +#import "FBRouteRequest.h" +#import "FBRuntimeUtils.h" +#import "FBSession.h" +#import "FBTCPSocket.h" +#import "FBUnknownCommands.h" +#import "FBConfiguration.h" +#import "FBLogger.h" + +#import "XCUIDevice+FBHelpers.h" + +static NSString *const FBServerURLBeginMarker = @"ServerURLHere->"; +static NSString *const FBServerURLEndMarker = @"<-ServerURLHere"; + +@interface FBHTTPConnection : RoutingConnection +@end + +@implementation FBHTTPConnection + +- (void)handleResourceNotFound +{ + [FBLogger logFmt:@"Received request for %@ which we do not handle", self.requestURI]; + [super handleResourceNotFound]; +} + +@end + + +@interface FBWebServer () +@property (nonatomic, strong) FBExceptionHandler *exceptionHandler; +@property (nonatomic, strong) RoutingHTTPServer *server; +@property (atomic, assign) BOOL keepAlive; +@property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster; +@end + +@implementation FBWebServer + ++ (NSArray> *)collectCommandHandlerClasses +{ + NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler)); + NSMutableArray *handlers = [NSMutableArray array]; + for (Class aClass in handlersClasses) { + if ([aClass respondsToSelector:@selector(shouldRegisterAutomatically)]) { + if (![aClass shouldRegisterAutomatically]) { + continue; + } + } + [handlers addObject:aClass]; + } + return handlers.copy; +} + +- (void)startServing +{ + [FBLogger logFmt:@"Built at %s %s", __DATE__, __TIME__]; + self.exceptionHandler = [FBExceptionHandler new]; + [self startHTTPServer]; + [self initScreenshotsBroadcaster]; + + self.keepAlive = YES; + NSRunLoop *runLoop = [NSRunLoop mainRunLoop]; + while (self.keepAlive && + [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); +} + +- (void)startHTTPServer +{ + self.server = [[RoutingHTTPServer alloc] init]; + [self.server setRouteQueue:dispatch_get_main_queue()]; + [self.server setDefaultHeader:@"Server" value:@"WebDriverAgent/1.0"]; + [self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"]; + [self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"]; + [self.server setConnectionClass:[FBHTTPConnection self]]; + + [self registerRouteHandlers:[self.class collectCommandHandlerClasses]]; + [self registerServerKeyRouteHandlers]; + + NSRange serverPortRange = FBConfiguration.bindingPortRange; + NSError *error; + BOOL serverStarted = NO; + + for (NSUInteger index = 0; index < serverPortRange.length; index++) { + NSInteger port = serverPortRange.location + index; + [self.server setPort:(UInt16)port]; + + serverStarted = [self attemptToStartServer:self.server onPort:port withError:&error]; + if (serverStarted) { + break; + } + + [FBLogger logFmt:@"Failed to start web server on port %ld with error %@", (long)port, [error description]]; + } + + if (!serverStarted) { + [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]]; + abort(); + } + [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, [XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"localhost", [self.server port], FBServerURLEndMarker]; +} + +- (void)initScreenshotsBroadcaster +{ + [self readMjpegSettingsFromEnv]; + self.screenshotsBroadcaster = [[FBTCPSocket alloc] + initWithPort:(uint16_t)FBConfiguration.mjpegServerPort]; + self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init]; + NSError *error; + if (![self.screenshotsBroadcaster startWithError:&error]) { + [FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description]; + self.screenshotsBroadcaster = nil; + } +} + +- (void)stopScreenshotsBroadcaster +{ + if (nil == self.screenshotsBroadcaster) { + return; + } + + [self.screenshotsBroadcaster stop]; +} + +- (void)readMjpegSettingsFromEnv +{ + NSDictionary *env = NSProcessInfo.processInfo.environment; + NSString *scalingFactor = [env objectForKey:@"MJPEG_SCALING_FACTOR"]; + if (scalingFactor != nil && [scalingFactor length] > 0) { + [FBConfiguration setMjpegScalingFactor:[scalingFactor floatValue]]; + } + NSString *screenshotQuality = [env objectForKey:@"MJPEG_SERVER_SCREENSHOT_QUALITY"]; + if (screenshotQuality != nil && [screenshotQuality length] > 0) { + [FBConfiguration setMjpegServerScreenshotQuality:[screenshotQuality integerValue]]; + } +} + +- (void)stopServing +{ + [FBSession.activeSession kill]; + [self stopScreenshotsBroadcaster]; + if (self.server.isRunning) { + [self.server stop:NO]; + } + self.keepAlive = NO; +} + +- (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port withError:(NSError **)error +{ + server.port = (UInt16)port; + NSError *innerError = nil; + BOOL started = [server start:&innerError]; + if (!started) { + if (!error) { + return NO; + } + + NSString *description = @"Unknown Error when Starting server"; + if ([innerError.domain isEqualToString:NSPOSIXErrorDomain] && innerError.code == EADDRINUSE) { + description = [NSString stringWithFormat:@"Unable to start web server on port %ld", (long)port]; + } + return + [[[[FBErrorBuilder builder] + withDescription:description] + withInnerError:innerError] + buildError:error]; + } + return YES; +} + +- (void)registerRouteHandlers:(NSArray *)commandHandlerClasses +{ + for (Class commandHandler in commandHandlerClasses) { + NSArray *routes = [commandHandler routes]; + for (FBRoute *route in routes) { + [self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) { + NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL]; + FBRouteRequest *routeParams = [FBRouteRequest + routeRequestWithURL:request.url + parameters:request.params + arguments:arguments ?: @{} + ]; + + [FBLogger verboseLog:routeParams.description]; + + @try { + [route mountRequest:routeParams intoResponse:response]; + } + @catch (NSException *exception) { + [self handleException:exception forResponse:response]; + } + }]; + } + } +} + +- (void)handleException:(NSException *)exception forResponse:(RouteResponse *)response +{ + [self.exceptionHandler handleException:exception forResponse:response]; +} + +- (void)registerServerKeyRouteHandlers +{ + [self.server get:@"/health" withBlock:^(RouteRequest *request, RouteResponse *response) { + [response respondWithString:@"Health Check

I-AM-ALIVE

"]; + }]; + + NSString *calibrationPage = @"" + "{\"x\":null,\"y\":null}" + "
" + "" + "
" + ""; + [self.server get:@"/calibrate" withBlock:^(RouteRequest *request, RouteResponse *response) { + [response respondWithString:calibrationPage]; + }]; + + [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { + [response respondWithString:@"Shutting down"]; + [self.delegate webServerDidRequestShutdown:self]; + }]; + + [self registerRouteHandlers:@[FBUnknownCommands.class]]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h new file mode 100644 index 0000000..dd35a37 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol FBXCAccessibilityElement + +@property(readonly) id payload; // @synthesize payload=_payload; +@property(readonly) int processIdentifier; // @synthesize processIdentifier=_processIdentifier; +@property(readonly) const struct __AXUIElement *AXUIElement; // @synthesize AXUIElement=_axElement; +@property(readonly, getter=isNative) BOOL native; + ++ (id)elementWithAXUIElement:(struct __AXUIElement *)arg1; ++ (id)elementWithProcessIdentifier:(int)arg1; ++ (id)deviceElement; ++ (id)mockElementWithProcessIdentifier:(int)arg1 payload:(id)arg2; ++ (id)mockElementWithProcessIdentifier:(int)arg1; + +- (id)initWithMockProcessIdentifier:(int)arg1 payload:(id)arg2; +- (id)initWithAXUIElement:(struct __AXUIElement *)arg1; +- (id)init; + +@end + +BOOL FBIsAXElementEqualToOther(id first, id second); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m new file mode 100644 index 0000000..39f2291 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCAccessibilityElement.h" + +#import "FBElementUtils.h" + +BOOL FBIsAXElementEqualToOther(id first, id second) +{ + return nil != second && [[FBElementUtils uidWithAccessibilityElement:first] + isEqualToString:([FBElementUtils uidWithAccessibilityElement:second] ?: @"")]; +} diff --git a/WebDriverAgentLib/Routing/FBXCDeviceEvent.h b/WebDriverAgentLib/Routing/FBXCDeviceEvent.h new file mode 100644 index 0000000..64139f9 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCDeviceEvent.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol FBXCDeviceEvent + +@property unsigned long long type; // @synthesize type=_type; +@property double rotation; // @synthesize rotation=_rotation; +@property double duration; // @synthesize duration=_duration; +@property unsigned int usage; // @synthesize usage=_usage; +@property unsigned int eventPage; // @synthesize eventPage=_eventPage; +@property(readonly) BOOL isButtonHoldEvent; + ++ (id)deviceEventForDigitalCrownRotation:(double)arg1 velocity:(double)arg2; ++ (id)deviceEventWithPage:(unsigned int)arg1 usage:(unsigned int)arg2 duration:(double)arg3; + +- (void)dispatch; + +@end + +_Nullable id FBCreateXCDeviceEvent(unsigned int page, + unsigned int usage, + double duration, + NSError **error); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBXCDeviceEvent.m b/WebDriverAgentLib/Routing/FBXCDeviceEvent.m new file mode 100644 index 0000000..6a673e1 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCDeviceEvent.m @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCDeviceEvent.h" + +#import "FBErrorBuilder.h" + +id FBCreateXCDeviceEvent(unsigned int page, + unsigned int usage, + double duration, + NSError **error) +{ + Class xcDeviceEventClass = NSClassFromString(@"XCDeviceEvent"); + if (nil == xcDeviceEventClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCDeviceEvent class"] + buildError:error]; + return nil; + } + SEL deviceEventFactorySelector = NSSelectorFromString(@"deviceEventWithPage:usage:duration:"); + if (![xcDeviceEventClass respondsToSelector:deviceEventFactorySelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'deviceEventWithPage:usage:duration:' factory method is not found on XCDeviceEvent class"] + buildError:error]; + return nil; + } + NSMethodSignature *deviceEventFactorySignature = [xcDeviceEventClass methodSignatureForSelector:deviceEventFactorySelector]; + NSInvocation *deviceEventFactoryInvocation = [NSInvocation invocationWithMethodSignature:deviceEventFactorySignature]; + [deviceEventFactoryInvocation setSelector:deviceEventFactorySelector]; + [deviceEventFactoryInvocation setArgument:&page atIndex:2]; + [deviceEventFactoryInvocation setArgument:&usage atIndex:3]; + [deviceEventFactoryInvocation setArgument:&duration atIndex:4]; + [deviceEventFactoryInvocation invokeWithTarget:xcDeviceEventClass]; + id __unsafe_unretained instance; + [deviceEventFactoryInvocation getReturnValue:&instance]; + return instance; +} diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshot.h b/WebDriverAgentLib/Routing/FBXCElementSnapshot.h new file mode 100644 index 0000000..500d9a8 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshot.h @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@protocol FBXCAccessibilityElement; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FBXCElementSnapshot + +@property BOOL hasFocus; // @synthesize hasFocus=_hasFocus; +@property BOOL hasKeyboardFocus; // @synthesize hasKeyboardFocus=_hasKeyboardFocus; +@property(copy) NSDictionary *additionalAttributes; // @synthesize additionalAttributes=_additionalAttributes; +@property(copy) NSArray *userTestingAttributes; // @synthesize userTestingAttributes=_userTestingAttributes; +@property unsigned long long traits; // @synthesize traits=_traits; +@property BOOL isMainWindow; // @synthesize isMainWindow=_isMainWindow; +@property(copy) NSArray *children; // @synthesize children=_children; +@property id parent; // @synthesize parent=_parent; +@property(retain) id parentAccessibilityElement; // @synthesize parentAccessibilityElement=_parentAccessibilityElement; +@property(retain) id accessibilityElement; // @synthesize accessibilityElement=_accessibilityElement; +@property(readonly) NSArray *suggestedHitpoints; +@property(readonly) struct CGRect visibleFrame; +@property(readonly) id scrollView; +@property(readonly, copy) NSString *truncatedValueString; +@property(readonly) long long depth; +@property(readonly, copy) id pathFromRoot; +@property(readonly) BOOL isTopLevelTouchBarElement; +@property(readonly) BOOL isTouchBarElement; +@property(readonly, copy) NSString *sparseTreeDescription; +@property(readonly, copy) NSString *compactDescription; +@property(readonly, copy) NSString *pathDescription; +@property(readonly) NSString *recursiveDescriptionIncludingAccessibilityElement; +@property(readonly) NSString *recursiveDescription; +@property(readonly, copy) NSArray *identifiers; +@property(nonatomic) unsigned long long generation; // @synthesize generation=_generation; +/*! DO NOT USE DIRECTLY! */ +@property(nonatomic) XCUIApplication *application; // @synthesize application=_application; +/*! DO NOT USE DIRECTLY! */ +@property(readonly) struct CGPoint hitPointForScrolling; +/*! DO NOT USE DIRECTLY! Please use fb_hitPoint instead */ +@property(readonly) struct CGPoint hitPoint; + +- (id)_uniquelyIdentifyingObjectiveCCode; +- (id)_uniquelyIdentifyingSwiftCode; +- (BOOL)_isAncestorOfElement:(id)arg1; +- (BOOL)_isDescendantOfElement:(id)arg1; +- (BOOL)_frameFuzzyMatchesElement:(id)arg1; +- (BOOL)_fuzzyMatchesElement:(id)arg1; +- (BOOL)_matchesElement:(id)arg1; +- (BOOL)matchesTreeWithRoot:(id)arg1; +- (void)mergeTreeWithSnapshot:(id)arg1; +- (id)_childMatchingElement:(id)arg1; +- (NSArray> *)_allDescendants; +- (BOOL)hasDescendantMatchingFilter:(CDUnknownBlockType)arg1; +- (NSArray> *)descendantsByFilteringWithBlock:(BOOL(^)(id snapshot))block; +- (id)elementSnapshotMatchingAccessibilityElement:(id)arg1; +- (void)enumerateDescendantsUsingBlock:(void(^)(id snapshot))block; +- (id)recursiveDescriptionWithIndent:(id)arg1 includeAccessibilityElement:(BOOL)arg2; +- (id)init; +- (struct CGPoint)hostingAndOrientationTransformedPoint:(struct CGPoint)arg1; +- (struct CGPoint)_transformPoint:(struct CGPoint)arg1 windowContextID:(id)arg2 windowDisplayID:(id)arg3; +- (id)hitTest:(struct CGPoint)arg1; + +// Available since Xcode 10 +- (id)hitPoint:(NSError **)error; + +// Since Xcode 10.2 ++ (id)axAttributesForElementSnapshotKeyPaths:(id)arg1 isMacOS:(_Bool)arg2; +// Since Xcode 10.0 ++ (NSArray *)sanitizedElementSnapshotHierarchyAttributesForAttributes:(nullable NSArray *)arg1 + isMacOS:(_Bool)arg2; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshot.m b/WebDriverAgentLib/Routing/FBXCElementSnapshot.m new file mode 100644 index 0000000..5fe6020 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshot.m @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshot.h" diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h new file mode 100644 index 0000000..2309d6b --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshot.h" +#import "FBElement.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXCElementSnapshotWrapper : NSObject + +/*!Wrapped snapshot instance */ +@property (nonatomic, readonly) id snapshot; + +/** + Wraps the given snapshot.. If the given snapshot is already wrapped then the result remains unchanged. + + @param snapshot snapshot instance to wrap + @returns wrapper instance + */ ++ (nullable instancetype)ensureWrapped:(nullable id)snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m new file mode 100644 index 0000000..4b3e0e7 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCElementSnapshotWrapper.h" + +#import "FBElementUtils.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-protocol-property-synthesis" +#pragma clang diagnostic ignored "-Wprotocol" + +@implementation FBXCElementSnapshotWrapper + +- (instancetype)initWithSnapshot:(id)snapshot; +{ + self->_snapshot = snapshot; + return self; +} + ++ (instancetype)ensureWrapped:(id)snapshot +{ + if (nil == snapshot) { + return nil; + } + return [(NSObject *)snapshot isKindOfClass:self.class] + ? (FBXCElementSnapshotWrapper *)snapshot + : [[FBXCElementSnapshotWrapper alloc] initWithSnapshot:snapshot]; +} + +// Attributes are queried most often, +// so we prefer them to have direct accessors defined here +// rather than to use message forwarding via forwardingTargetForSelector, +// which is slow + +- (NSString *)identifier +{ + return self.snapshot.identifier; +} + +- (CGRect)frame +{ + return self.snapshot.frame; +} + +- (id)value +{ + return self.snapshot.value; +} + +- (NSString *)title +{ + return self.snapshot.title; +} + +- (NSString *)label +{ + return self.snapshot.label; +} + +- (XCUIElementType)elementType +{ + return self.snapshot.elementType; +} + +- (BOOL)isEnabled +{ + return self.snapshot.enabled; +} + +- (XCUIUserInterfaceSizeClass)horizontalSizeClass +{ + return self.snapshot.horizontalSizeClass; +} + +- (XCUIUserInterfaceSizeClass)verticalSizeClass +{ + return self.snapshot.verticalSizeClass; +} + +- (NSString *)placeholderValue +{ + return self.snapshot.placeholderValue; +} + +- (BOOL)isSelected +{ + return self.snapshot.selected; +} + +#if !TARGET_OS_OSX +- (BOOL)hasFocus +{ + return self.snapshot.hasFocus; +} +#endif + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + static dispatch_once_t onceToken; + static NSSet *names; + dispatch_once(&onceToken, ^{ + names = [FBElementUtils selectorNamesWithProtocol:@protocol(FBXCElementSnapshot)]; + }); + return [names containsObject:NSStringFromSelector(aSelector)] ? self.snapshot : nil; +} + +@end + +#pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h new file mode 100644 index 0000000..b5ffa59 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Converts accessibility traits bitmask to an array of string representations + @param traits The accessibility traits bitmask + @return Array of strings representing the accessibility traits + */ +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m new file mode 100644 index 0000000..74ce9ec --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAccessibilityTraits.h" + +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits) { + NSMutableArray *traitStringsArray; + NSNumber *key; + + static NSDictionary *traitsMapping; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSMutableDictionary *mapping = [@{ + @(UIAccessibilityTraitNone): @"None", + @(UIAccessibilityTraitButton): @"Button", + @(UIAccessibilityTraitLink): @"Link", + @(UIAccessibilityTraitHeader): @"Header", + @(UIAccessibilityTraitSearchField): @"SearchField", + @(UIAccessibilityTraitImage): @"Image", + @(UIAccessibilityTraitSelected): @"Selected", + @(UIAccessibilityTraitPlaysSound): @"PlaysSound", + @(UIAccessibilityTraitKeyboardKey): @"KeyboardKey", + @(UIAccessibilityTraitStaticText): @"StaticText", + @(UIAccessibilityTraitSummaryElement): @"SummaryElement", + @(UIAccessibilityTraitNotEnabled): @"NotEnabled", + @(UIAccessibilityTraitUpdatesFrequently): @"UpdatesFrequently", + @(UIAccessibilityTraitStartsMediaSession): @"StartsMediaSession", + @(UIAccessibilityTraitAdjustable): @"Adjustable", + @(UIAccessibilityTraitAllowsDirectInteraction): @"AllowsDirectInteraction", + @(UIAccessibilityTraitCausesPageTurn): @"CausesPageTurn", + @(UIAccessibilityTraitTabBar): @"TabBar" + } mutableCopy]; + + #if __clang_major__ >= 16 + // Add iOS 17.0 specific traits if available + if (@available(iOS 17.0, *)) { + [mapping addEntriesFromDictionary:@{ + @(UIAccessibilityTraitToggleButton): @"ToggleButton", + @(UIAccessibilityTraitSupportsZoom): @"SupportsZoom" + }]; + } + #endif + + traitsMapping = [mapping copy]; + }); + + traitStringsArray = [NSMutableArray array]; + for (key in traitsMapping) { + if (traits & [key unsignedLongLongValue] && nil != traitsMapping[key]) { + [traitStringsArray addObject:(id)traitsMapping[key]]; + } + } + + return [traitStringsArray copy]; +} diff --git a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h new file mode 100644 index 0000000..a65bd38 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +@protocol FBXCAccessibilityElement; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBActiveAppDetectionPoint : NSObject + +@property (nonatomic) CGPoint coordinates; + +/** + * Retrieves singleton representation of the current class + */ ++ (instancetype)sharedInstance; + +/** + * Calculates the accessbility element which is located at the given screen coordinates + * + * @param point The screen coordinates + * @returns The retrieved accessbility element or nil if it cannot be detected + */ ++ (nullable id)axElementWithPoint:(CGPoint)point; + +/** + * Retrieves the accessbility element for the current screen point + * + * @returns The retrieved accessbility element or nil if it cannot be detected + */ +- (nullable id)axElement; + +/** + * Sets the coordinates for the current screen point + * + * @param coordinatesStr The coordinates string in `x,y` format. x and y can be any float numbers + * @param error Is assigned to the actual error object if coordinates cannot be set + * @returns YES if the coordinates were successfully set + */ +- (BOOL)setCoordinatesWithString:(NSString *)coordinatesStr error:(NSError **)error; + +/** + * Retrieves the coordinates of the current screen point in string representation + * + * @returns Point coordinates as `x,y` string + */ +- (NSString *)stringCoordinates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m new file mode 100644 index 0000000..2f05ab8 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m @@ -0,0 +1,87 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "FBActiveAppDetectionPoint.h" + +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCTestManager_ManagerInterface-Protocol.h" + +@implementation FBActiveAppDetectionPoint + +- (instancetype)init { + if ((self = [super init])) { + CGSize screenSize = [UIScreen mainScreen].bounds.size; + // Consider the element, which is located close to the top left corner of the screen the on-screen one. + CGFloat pointDistance = MIN(screenSize.width, screenSize.height) * (CGFloat) 0.2; + _coordinates = CGPointMake(pointDistance, pointDistance); + } + return self; +} + ++ (instancetype)sharedInstance +{ + static FBActiveAppDetectionPoint *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + ++ (id)axElementWithPoint:(CGPoint)point +{ + __block id onScreenElement = nil; + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [proxy _XCT_requestElementAtPoint:point + reply:^(id element, NSError *error) { + if (nil == error) { + onScreenElement = element; + } else { + [FBLogger logFmt:@"Cannot request the screen point at %@", NSStringFromCGPoint(point)]; + } + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC))); + return onScreenElement; +} + +- (id)axElement +{ + return [self.class axElementWithPoint:self.coordinates]; +} + +- (BOOL)setCoordinatesWithString:(NSString *)coordinatesStr error:(NSError **)error +{ + NSArray *screenPointCoords = [coordinatesStr componentsSeparatedByString:@","]; + if (screenPointCoords.count != 2) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The screen point coordinates should be separated by a single comma character. Got '%@' instead", coordinatesStr] + buildError:error]; + } + NSString *strX = [screenPointCoords.firstObject stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceAndNewlineCharacterSet]; + NSString *strY = [screenPointCoords.lastObject stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if (0 == strX.length || 0 == strY.length) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Both screen point coordinates should be valid numbers. Got '%@' instead", coordinatesStr] + buildError:error]; + } + self.coordinates = CGPointMake((CGFloat) strX.doubleValue, (CGFloat) strY.doubleValue); + return YES; +} + +- (NSString *)stringCoordinates +{ + return [NSString stringWithFormat:@"%.2f,%.2f", self.coordinates.x, self.coordinates.y]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.h b/WebDriverAgentLib/Utilities/FBAlertsMonitor.h new file mode 100644 index 0000000..069a734 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class FBAlert, XCUIApplication; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FBAlertsMonitorDelegate + +/** + The callback which is invoked when an unexpected on-screen alert is shown + + @param alert The instance of the current alert + */ +- (void)didDetectAlert:(FBAlert *)alert; + +@end + +@interface FBAlertsMonitor : NSObject + +/*! The delegate which decides on what to do when an alert is detected */ +@property (nonatomic, nullable, weak) id delegate; + +/** + Creates an instance of alerts monitor. + The monitoring is done on the main thread and is disabled unless `enable` is called. + + @return Alerts monitor instance + */ +- (instancetype)init; + +/** + Enables alerts monitoring + */ +- (void)enable; + +/** + Disables alerts monitoring + */ +- (void)disable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m new file mode 100644 index 0000000..3dd2211 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAlertsMonitor.h" + +#import "FBAlert.h" +#import "FBLogger.h" +#import "XCUIApplication+FBAlert.h" +#import "XCUIApplication+FBHelpers.h" + +static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0; + +@interface FBAlertsMonitor() + +@property (atomic) BOOL isMonitoring; + +@end + +@implementation FBAlertsMonitor + +- (instancetype)init +{ + if ((self = [super init])) { + _isMonitoring = NO; + _delegate = nil; + } + return self; +} + +- (void)scheduleNextTick +{ + if (!self.isMonitoring) { + return; + } + + dispatch_time_t delta = (int64_t)(FB_MONTORING_INTERVAL * NSEC_PER_SEC); + + if (nil == self.delegate) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delta), dispatch_get_main_queue(), ^{ + [self scheduleNextTick]; + }); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *activeApps = XCUIApplication.fb_activeApplications; + for (XCUIApplication *activeApp in activeApps) { + XCUIElement *alertElement = nil; + @try { + alertElement = activeApp.fb_alertElement; + if (nil != alertElement) { + [self.delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols]; + } + if (nil != alertElement) { + break; + } + } + + if (self.isMonitoring) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delta), dispatch_get_main_queue(), ^{ + [self scheduleNextTick]; + }); + } + }); +} + +- (void)enable +{ + if (self.isMonitoring) { + return; + } + + self.isMonitoring = YES; + [self scheduleNextTick]; +} + +- (void)disable +{ + if (!self.isMonitoring) { + return; + } + + self.isMonitoring = NO; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h new file mode 100644 index 0000000..7dc7c42 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementCache.h" +#import "FBXCElementSnapshot.h" +#import "XCUIApplication.h" +#import "XCSynthesizedEventRecord.h" + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_TV +@interface FBBaseActionItem : NSObject + +/*! Raw JSON representation of the corresponding action item */ +@property (nonatomic) NSDictionary *actionItem; +/*! Current application instance */ +@property (nonatomic) XCUIApplication *application; +/*! Action offset in the chain in milliseconds */ +@property (nonatomic) double offset; + +/** + Get the name of the corresponding raw action item. This method is expected to be overriden in subclasses. + + @return The corresponding action item key in object's raw JSON reprsentation + */ ++ (NSString *)actionName; + +/** + Add the current gesture to XCPointerEventPath instance. This method is expected to be overriden in subclasses. + + @param eventPath The destination XCPointerEventPath instance. If nil value is passed then a new XCPointerEventPath instance is going to be created + @param allItems The existing actions chain to be transformed into event path + @param currentItemIndex The index of the current item in allItems array + @param error If there is an error, upon return contains an NSError object that describes the problem + @return the constructed XCPointerEventPath instance or nil in case of failure + */ +- (nullable NSArray *)addToEventPath:(nullable XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error; + +@end + + +@interface FBBaseGestureItem : FBBaseActionItem + +/*! Absolute position on the screen where the gesure should be performed */ +@property (nonatomic) XCUICoordinate *atPosition; +/*! Gesture duration in milliseconds */ +@property (nonatomic) double duration; + +/** + Calculate absolute gesture position on the screen based on provided element and positionOffset values. + + @param element The element instance to perform the gesture on. If element equals to nil then positionOffset is considered as absolute coordinates + @param positionOffset The actual coordinate offset. If this calue equals to nil then element's hitpoint is taken as gesture position. If element is not nil then this offset is calculated relatively to the top-left cordner of the element's position + @param error If there is an error, upon return contains an NSError object that describes the problem + @return Adbsolute gesture position on the screen or nil if the calculation fails (for example, the element is invisible) + */ +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error; + +@end + + +@interface FBBaseActionItemsChain : NSObject + +/*! All gesture items collected in the chain */ +@property (readonly, nonatomic) NSMutableArray *items; +/*! Total length of all the gestures in the chain in milliseconds */ +@property (nonatomic) double durationOffset; + +/** + Add a new gesture item to the current chain. The method is expected to be overriden in subclasses. + + @param item The actual gesture instance to be added + */ +- (void)addItem:(FBBaseActionItem *)item; + +/** + Represents the chain as XCPointerEventPath instance. + + @param error If there is an error, upon return contains an NSError object that describes the problem + @return The constructed array of XCPointerEventPath instances or nil if there was a failure + */ +- (nullable NSArray *)asEventPathsWithError:(NSError **)error; + +@end + + +@interface FBBaseActionsSynthesizer : NSObject + +/*! Raw actions chain received from request's JSON */ +@property (readonly, nonatomic) NSArray *actions; +/*! Current application instance */ +@property (readonly, nonatomic) XCUIApplication *application; +/*! Current elements cache */ +@property (readonly, nonatomic, nullable) FBElementCache *elementCache; + +/** + Initializes actions synthesizer. This initializer should be used only by subclasses. + + @param actions The raw actions chain received from request's JSON. The format of this chain is defined by the standard, implemented in the correspoding subclass. + @param application Current application instance + @param elementCache Elements cache, which is used to replace elements references in the chain with their instances. We assume the chain already contains element instances if this parameter is set to nil + @param error If there is an error, upon return contains an NSError object that describes the problem + @return The corresponding synthesizer instance or nil in case of failure (for example if `actions` is nil or empty) + */ +- (nullable instancetype)initWithActions:(NSArray *)actions + forApplication:(XCUIApplication *)application + elementCache:(nullable FBElementCache *)elementCache + error:(NSError **)error; + +/** + Synthesizes XCTest-compatible event record to be performed in the UI. This method is supposed to be overriden by subclasses. + + @param error If there is an error, upon return contains an NSError object that describes the problem + @return The generated event record or nil in case of failure + */ +- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m new file mode 100644 index 0000000..57f9d2d --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBBaseActionsSynthesizer.h" + +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBCaching.h" +#import "XCPointerEventPath.h" +#import "XCSynthesizedEventRecord.h" +#import "XCUIElement+FBUtilities.h" + +#if !TARGET_OS_TV +@implementation FBBaseActionItem + ++ (NSString *)actionName +{ + @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; + return nil; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; + return nil; +} + +@end + +@implementation FBBaseGestureItem + +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error +{ + if (nil == element) { + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); + // Only absolute offset is defined + return [[self.application coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; + } + + // The offset relative to the element is defined + if (nil == positionOffset) { + if (element.hittable) { + // short circuit element hitpoint + return element.hitPointCoordinate; + } + [FBLogger logFmt:@"Will use the frame of '%@' for hit point calculation instead", element.debugDescription]; + } + if (CGRectIsEmpty(element.frame)) { + [FBLogger log:self.application.fb_descriptionRepresentation]; + NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", + element.description]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + if (nil == positionOffset) { + return [element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; + } + + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); + // TODO: Shall we throw an exception if hitPoint is out of the element frame? + return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; +} + +@end + + +@implementation FBBaseActionItemsChain + +- (instancetype)init +{ + self = [super init]; + if (self) { + _items = [NSMutableArray array]; + _durationOffset = 0.0; + } + return self; +} + +- (void)addItem:(FBBaseActionItem *)item __attribute__((noreturn)) +{ + @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; +} + +- (nullable NSArray *)asEventPathsWithError:(NSError **)error +{ + if (0 == self.items.count) { + if (error) { + *error = [[FBErrorBuilder.builder withDescription:@"Action items list cannot be empty"] build]; + } + return nil; + } + + NSMutableArray *result = [NSMutableArray array]; + XCPointerEventPath *previousEventPath = nil; + XCPointerEventPath *currentEventPath = nil; + NSUInteger index = 0; + for (FBBaseActionItem *item in self.items.copy) { + NSArray *currentEventPaths = [item addToEventPath:currentEventPath + allItems:self.items.copy + currentItemIndex:index++ + error:error]; + if (currentEventPaths == nil) { + return nil; + } + + currentEventPath = currentEventPaths.lastObject; + if (nil == currentEventPath) { + currentEventPath = previousEventPath; + } else if (currentEventPath != previousEventPath) { + [result addObjectsFromArray:currentEventPaths]; + previousEventPath = currentEventPath; + } + } + return result.copy; +} + +@end + + +@implementation FBBaseActionsSynthesizer + +- (instancetype)initWithActions:(NSArray *)actions + forApplication:(XCUIApplication *)application + elementCache:(nullable FBElementCache *)elementCache + error:(NSError **)error +{ + self = [super init]; + if (self) { + if ((nil == actions || 0 == actions.count) && error) { + *error = [[FBErrorBuilder.builder withDescription:@"Actions list cannot be empty"] build]; + return nil; + } + _actions = actions; + _application = application; + _elementCache = elementCache; + } + return self; +} + +- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error +{ + @throw [[FBErrorBuilder.builder withDescription:@"Override synthesizeWithError method in subclasses"] build]; + return nil; +} + +@end +#endif diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgentLib/Utilities/FBCapabilities.h new file mode 100644 index 0000000..d1339c7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** Whether to use alternative elements visivility detection method */ +extern NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION; +/** Set the maximum amount of characters that could be typed within a minute (60 by default) */ +extern NSString* const FB_CAP_MAX_TYPING_FREQUENCY; +/** this setting was needed for some legacy stuff */ +extern NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER; +/** Whether to disable screneshots that XCTest automaticallly creates after each step */ +extern NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS; +/** Whether to terminate the application under test after the session ends */ +extern NSString* const FB_CAP_SHOULD_TERMINATE_APP; +/** The maximum amount of seconds to wait for the event loop to become idle */ +extern NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC; +/** Bundle identifier of the application to run the test for */ +extern NSString* const FB_CAP_BUNDLE_ID; +/** + Usually an URL used as initial link to run Mobile Safari, but could be any other deep link. + This might also work together with `FB_CAP_BUNLDE_ID`, which tells XCTest to open + the given deep link in the particular app. + Only works since iOS 16.4 + */ +extern NSString* const FB_CAP_INITIAL_URL; +/** Whether to enforrce (re)start of the application under test on session startup */ +extern NSString* const FB_CAP_FORCE_APP_LAUNCH; +/** Whether to wait for quiescence before starting interaction with apps laucnhes in scope of the test session */ +extern NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE; +/** Array of command line arguments to be passed to the application under test */ +extern NSString* const FB_CAP_ARGUMENTS; +/** Dictionary of environment variables to be passed to the application under test */ +extern NSString* const FB_CAP_ENVIRNOMENT; +/** Whether to use native XCTest caching strategy */ +extern NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY; +/** Whether to enforce software keyboard presence on simulator */ +extern NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE; +/** Sets the application state change timeout for the initial app startup */ +extern NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC; diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.m b/WebDriverAgentLib/Utilities/FBCapabilities.m new file mode 100644 index 0000000..351f23e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBCapabilities.m @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBCapabilities.h" + +NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION = @"shouldUseTestManagerForVisibilityDetection"; +NSString* const FB_CAP_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; +NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER = @"shouldUseSingletonTestManager"; +NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS = @"disableAutomaticScreenshots"; +NSString* const FB_CAP_SHOULD_TERMINATE_APP = @"shouldTerminateApp"; +NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC = @"eventloopIdleDelaySec"; +NSString* const FB_CAP_BUNDLE_ID = @"bundleId"; +NSString* const FB_CAP_INITIAL_URL = @"initialUrl"; +NSString* const FB_CAP_FORCE_APP_LAUNCH = @"forceAppLaunch"; +NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE = @"shouldWaitForQuiescence"; +NSString* const FB_CAP_ARGUMENTS = @"arguments"; +NSString* const FB_CAP_ENVIRNOMENT = @"environment"; +NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY = @"useNativeCachingStrategy"; +NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE = @"forceSimulatorSoftwareKeyboardPresence"; +NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC = @"appLaunchStateTimeoutSec"; diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h new file mode 100644 index 0000000..c2ca9c5 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface FBAbstractPredicateItem : NSObject + +/*! The actual predicate value of an item */ +@property (nonatomic, readonly) NSPredicate *value; + +/** + Instance constructor, which allows to set item value on instance creation + + @param value the actual predicate value + @return FBAbstractPredicateItem instance + */ +- (instancetype)initWithValue:(NSPredicate *)value; + +@end + +@interface FBSelfPredicateItem : FBAbstractPredicateItem + +@end + +@interface FBDescendantPredicateItem : FBAbstractPredicateItem + +@end + +@interface FBClassChainItem : NSObject + +/*! Element's position */ +@property (readonly, nonatomic, nullable) NSNumber *position; +/*! Element's type */ +@property (readonly, nonatomic) XCUIElementType type; +/*! Whether an element is a descendant of the previos element */ +@property (readonly, nonatomic) BOOL isDescendant; +/*! The ordered list of matching predicates for the current element */ +@property (readonly, nonatomic) NSArray *predicates; + +/** + Instance constructor, which allows to set element type and position + + @param type on of supoported element types declared in XCUIElementType enum + @param position element position relative to its sibling element. Numeration + starts with 1. Zero value means that all sibling element should be selected. + Negative value means that numeration starts from the last element, for example + -1 is the last child element and -2 is the second last element. + nil value means that no element position has been set explicitly. + @param predicates the list of matching descendant/self predicates + @param isDescendant equals to YES if the element is a descendantt element of + the previous element in the chain. NO value means the element is the direct + child of the previous element + @return FBClassChainElement instance + */ +- (instancetype)initWithType:(XCUIElementType)type position:(nullable NSNumber *)position predicates:(NSArray *)predicates isDescendant:(BOOL)isDescendant; + +@end + +@interface FBClassChain : NSObject + +/*! Array of parsed chain items */ +@property (readonly, nonatomic, copy) NSArray *elements; + +/** + Instance constructor for parsed class chain instance + + @param elements an array of parsed chains elements + @return FBClassChain instance + */ +- (instancetype)initWithElements:(NSArray *)elements; + +@end + +@interface FBClassChainQueryParser : NSObject + +/** + Method used to interpret class chain queries + + @param classChainQuery class chain query as string. See the documentation of + XCUIElement+FBClassChain category for more details about the expected query format + @param error standard NSError object, which is going to be initializaed if + there are query parsing errors + @return list of parsed primitives packed to FBClassChainElement class or nil in case + there was parsing error (the parameter will be initialized with detailed error description in such case) + @throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute + */ ++ (nullable FBClassChain*)parseQuery:(NSString*)classChainQuery error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m new file mode 100644 index 0000000..933d0ad --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m @@ -0,0 +1,666 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBClassChainQueryParser.h" +#import "FBErrorBuilder.h" +#import "FBElementTypeTransformer.h" +#import "FBExceptions.h" +#import "NSPredicate+FBFormat.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FBBaseClassChainToken : NSObject + +@property (nonatomic) NSString *asString; +@property (nonatomic) NSUInteger previousItemsCountToOverride; + +@end + + +@interface FBClassNameToken : FBBaseClassChainToken + +@end + +@interface FBStarToken : FBBaseClassChainToken + +@end + +@interface FBDescendantMarkerToken : FBBaseClassChainToken + +@end + +@interface FBSplitterToken : FBBaseClassChainToken + +@end + +@interface FBOpeningBracketToken : FBBaseClassChainToken + +@end + +@interface FBClosingBracketToken : FBBaseClassChainToken + +@end + +@interface FBNumberToken : FBBaseClassChainToken + +@end + +@interface FBAbstractPredicateToken : FBBaseClassChainToken + +@property (nonatomic) BOOL isParsingCompleted; + ++ (NSString *)enclosingMarker; + +@end + +@interface FBSelfPredicateToken : FBAbstractPredicateToken + +@end + +@interface FBDescendantPredicateToken : FBAbstractPredicateToken + +@end + +NS_ASSUME_NONNULL_END + + +@implementation FBBaseClassChainToken + +- (id)init +{ + self = [super init]; + if (self) { + _asString = @""; + _previousItemsCountToOverride = 0; + } + return self; +} + +- (instancetype)initWithStringValue:(NSString *)stringValue +{ + self = [super init]; + if (self) { + _asString = stringValue; + } + return self; +} + ++ (NSCharacterSet *)allowedCharacters +{ + // This method is expected to be overriden by subclasses + return [NSCharacterSet characterSetWithCharactersInString:@""]; +} + ++ (NSUInteger)maxLength +{ + // This method is expected to be overriden by subclasses + return ULONG_MAX; +} + +- (NSArray *)followingTokens +{ + // This method is expected to be overriden by subclasses + return @[]; +} + ++ (BOOL)canConsumeCharacter:(unichar)character +{ + return [self.allowedCharacters characterIsMember:character]; +} + +- (void)appendChar:(unichar)character +{ + NSMutableString *value = [NSMutableString stringWithString:self.asString]; + [value appendFormat:@"%C", character]; + self.asString = value.copy;; +} + +- (nullable FBBaseClassChainToken*)followingTokenBasedOn:(unichar)character +{ + for (Class matchingTokenClass in self.followingTokens) { + if ([matchingTokenClass canConsumeCharacter:character]) { + return [[[matchingTokenClass alloc] init] nextTokenWithCharacter:character]; + } + } + return nil; +} + +- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character +{ + if ([self.class canConsumeCharacter:character] && self.asString.length < [self.class maxLength]) { + [self appendChar:character]; + return self; + } + return [self followingTokenBasedOn:character]; +} + +@end + + +@implementation FBClassNameToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet letterCharacterSet]; +} + +- (NSArray *)followingTokens +{ + return @[FBSplitterToken.class, FBOpeningBracketToken.class]; +} + +@end + +static NSString *const STAR_TOKEN = @"*"; +@implementation FBStarToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet characterSetWithCharactersInString:STAR_TOKEN]; +} + +- (NSArray *)followingTokens +{ + return @[FBSplitterToken.class, FBOpeningBracketToken.class]; +} + +- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character +{ + if ([self.class.allowedCharacters characterIsMember:character]) { + if (self.asString.length >= 1) { + FBDescendantMarkerToken *nextToken = [[FBDescendantMarkerToken alloc] initWithStringValue:[NSString stringWithFormat:@"%@%@", STAR_TOKEN, STAR_TOKEN]]; + nextToken.previousItemsCountToOverride = 1; + return nextToken; + } + [self appendChar:character]; + return self; + } + return [self followingTokenBasedOn:character]; +} + +@end + + +static NSString *const DESCENDANT_MARKER = @"**/"; +@implementation FBDescendantMarkerToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet characterSetWithCharactersInString:@"*/"]; +} + +- (NSArray *)followingTokens +{ + return @[FBClassNameToken.class, FBStarToken.class]; +} + ++ (NSUInteger)maxLength +{ + return 3; +} + +- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character +{ + if ([self.class.allowedCharacters characterIsMember:character] && self.asString.length <= self.class.maxLength) { + if (self.asString.length > 0 && ![DESCENDANT_MARKER hasPrefix:self.asString]) { + return nil; + } + if (self.asString.length < self.class.maxLength) { + [self appendChar:character]; + return self; + } + } + return [self followingTokenBasedOn:character]; +} + +@end + + +@implementation FBSplitterToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet characterSetWithCharactersInString:@"/"]; +} + +- (NSArray *)followingTokens +{ + return @[FBStarToken.class, FBClassNameToken.class]; +} + ++ (NSUInteger)maxLength +{ + return 1; +} + +@end + + +@implementation FBOpeningBracketToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet characterSetWithCharactersInString:@"["]; +} + +- (NSArray *)followingTokens +{ + return @[FBNumberToken.class, FBSelfPredicateToken.class, FBDescendantPredicateToken.class]; +} + ++ (NSUInteger)maxLength +{ + return 1; +} + +@end + + +@implementation FBNumberToken + ++ (NSCharacterSet *)allowedCharacters +{ + NSMutableCharacterSet *result = [NSMutableCharacterSet new]; + [result formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]]; + [result addCharactersInString:@"-"]; + return result.copy; +} + +- (NSArray *)followingTokens +{ + return @[FBClosingBracketToken.class]; +} + +@end + + +@implementation FBClosingBracketToken + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet characterSetWithCharactersInString:@"]"]; +} + +- (NSArray *)followingTokens +{ + return @[FBSplitterToken.class, FBOpeningBracketToken.class]; +} + ++ (NSUInteger)maxLength +{ + return 1; +} + +@end + +static NSString *const FBAbstractMethodInvocationException = @"FBAbstractMethodInvocationException"; + +@implementation FBAbstractPredicateToken + +- (id)init +{ + self = [super init]; + if (self) { + _isParsingCompleted = NO; + } + return self; +} + ++ (NSString *)enclosingMarker +{ + NSString *errMsg = [NSString stringWithFormat:@"The + (NSString *)enclosingMarker method is expected to be overriden by %@ class", NSStringFromClass(self.class)]; + @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil]; +} + ++ (NSCharacterSet *)allowedCharacters +{ + return [NSCharacterSet illegalCharacterSet].invertedSet; +} + +- (NSArray *)followingTokens +{ + return @[FBClosingBracketToken.class]; +} + ++ (BOOL)canConsumeCharacter:(unichar)character +{ + return [[NSCharacterSet characterSetWithCharactersInString:self.class.enclosingMarker] characterIsMember:character]; +} + +- (void)stripLastChar +{ + if (self.asString.length > 0) { + self.asString = [self.asString substringToIndex:self.asString.length - 1]; + } +} + +- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character +{ + NSString *currentChar = [NSString stringWithFormat:@"%C", character]; + if (!self.isParsingCompleted && [self.class.allowedCharacters characterIsMember:character]) { + if (0 == self.asString.length) { + if ([self.class.enclosingMarker isEqualToString:currentChar]) { + // Do not include enclosing character + return self; + } + } else if ([self.class.enclosingMarker isEqualToString:currentChar]) { + [self appendChar:character]; + self.isParsingCompleted = YES; + return self; + } + [self appendChar:character]; + return self; + } + if (self.isParsingCompleted) { + if ([currentChar isEqualToString:self.class.enclosingMarker]) { + // Escaped enclosing character has been detected. Do not finish parsing + self.isParsingCompleted = NO; + return self; + } else { + // Do not include enclosing character + [self stripLastChar]; + } + } + return [self followingTokenBasedOn:character]; +} + +@end + +@implementation FBSelfPredicateToken + ++ (NSString *)enclosingMarker +{ + return @"`"; +} + +@end + +@implementation FBDescendantPredicateToken + ++ (NSString *)enclosingMarker +{ + return @"$"; +} + +@end + + +@implementation FBClassChainItem + +- (instancetype)initWithType:(XCUIElementType)type position:(NSNumber *)position predicates:(NSArray *)predicates isDescendant:(BOOL)isDescendant +{ + self = [super init]; + if (self) { + _type = type; + _position = position; + _predicates = predicates; + _isDescendant = isDescendant; + } + return self; +} + +@end + + +@implementation FBClassChain + +- (instancetype)initWithElements:(NSArray *)elements +{ + self = [super init]; + if (self) { + _elements = elements; + } + return self; +} + +@end + + +@implementation FBClassChainQueryParser + +static NSNumberFormatter *numberFormatter = nil; + ++ (void)initialize { + if (nil == numberFormatter) { + numberFormatter = [[NSNumberFormatter alloc] init]; + numberFormatter.numberStyle = NSNumberFormatterDecimalStyle; + } +} + ++ (NSError *)tokenizationErrorWithIndex:(NSUInteger)index originalQuery:(NSString *)originalQuery +{ + NSString *description = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. Unexpected character detected at position %@:\n%@ <----", originalQuery, @(index + 1), [originalQuery substringToIndex:index + 1]]; + return [[FBErrorBuilder.builder withDescription:description] build]; +} + ++ (nullable NSArray *)tokenizedQueryWithQuery:(NSString*)classChainQuery error:(NSError **)error +{ + NSUInteger queryStringLength = classChainQuery.length; + FBBaseClassChainToken *token; + unichar firstCharacter = [classChainQuery characterAtIndex:0]; + if ([classChainQuery hasPrefix:DESCENDANT_MARKER]) { + token = [[FBDescendantMarkerToken alloc] initWithStringValue:DESCENDANT_MARKER]; + } else if ([FBClassNameToken canConsumeCharacter:firstCharacter]) { + token = [[FBClassNameToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]]; + } else if ([FBStarToken canConsumeCharacter:firstCharacter]) { + token = [[FBStarToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]]; + } else { + if (error) { + *error = [self.class tokenizationErrorWithIndex:0 originalQuery:classChainQuery]; + } + return nil; + } + NSMutableArray *result = [NSMutableArray array]; + FBBaseClassChainToken *nextToken = token; + for (NSUInteger charIdx = token.asString.length; charIdx < queryStringLength; ++charIdx) { + nextToken = [token nextTokenWithCharacter:[classChainQuery characterAtIndex:charIdx]]; + if (nil == nextToken) { + if (error) { + *error = [self.class tokenizationErrorWithIndex:charIdx originalQuery:classChainQuery]; + } + return nil; + } + if (nextToken != token) { + [result addObject:token]; + if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) { + NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count; + [result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)]; + } + token = nextToken; + } + } + if (nextToken) { + if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) { + NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count; + [result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)]; + } + [result addObject:nextToken]; + } + + FBBaseClassChainToken *lastToken = [result lastObject]; + if (!([lastToken isKindOfClass:FBClosingBracketToken.class] || + [lastToken isKindOfClass:FBClassNameToken.class] || + [lastToken isKindOfClass:FBStarToken.class])) { + if (error) { + *error = [self.class tokenizationErrorWithIndex:queryStringLength - 1 originalQuery:classChainQuery]; + } + return nil; + } + + return result.copy; +} + ++ (NSError *)compilationErrorWithQuery:(NSString *)originalQuery description:(NSString *)description +{ + NSString *fullDescription = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. %@", originalQuery, description]; + return [[FBErrorBuilder.builder withDescription:fullDescription] build]; +} + ++ (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray *)tokenizedQuery + originalQuery:(NSString *)originalQuery + error:(NSError **)error +{ + NSMutableArray *result = [NSMutableArray array]; + XCUIElementType chainElementType = XCUIElementTypeAny; + NSNumber *chainElementPosition = nil; + BOOL isTypeSet = NO; + BOOL isPositionSet = NO; + BOOL isDescendantSet = NO; + NSMutableArray *predicates = [NSMutableArray array]; + for (FBBaseClassChainToken *token in tokenizedQuery) { + if ([token isKindOfClass:FBClassNameToken.class]) { + if (isTypeSet) { + if (error) { + NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString]; + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + @try { + chainElementType = [FBElementTypeTransformer elementTypeWithTypeName:token.asString]; + isTypeSet = YES; + } @catch (NSException *e) { + if ([e.name isEqualToString:FBInvalidArgumentException]) { + if (error) { + NSString *description = [NSString stringWithFormat:@"'%@' class name is unknown to WDA", token.asString]; + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + @throw e; + } + } else if ([token isKindOfClass:FBStarToken.class]) { + if (isTypeSet) { + if (error) { + NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString]; + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + chainElementType = XCUIElementTypeAny; + isTypeSet = YES; + } else if ([token isKindOfClass:FBDescendantMarkerToken.class]) { + if (isDescendantSet) { + NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. Descendant markers cannot be duplicated.", token.asString]; + if (error) { + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + isTypeSet = NO; + isPositionSet = NO; + [predicates removeAllObjects]; + isDescendantSet = YES; + } else if ([token isKindOfClass:FBAbstractPredicateToken.class]) { + if (isPositionSet) { + NSString *description = [NSString stringWithFormat:@"Predicate value '%@' must be set before position value.", token.asString]; + if (error) { + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + if (!((FBAbstractPredicateToken *)token).isParsingCompleted) { + NSString *description = [NSString stringWithFormat:@"Cannot find the end of '%@' predicate value.", token.asString]; + if (error) { + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + NSPredicate *value = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate predicateWithFormat:token.asString]]; + if ([token isKindOfClass:FBSelfPredicateToken.class]) { + [predicates addObject:[[FBSelfPredicateItem alloc] initWithValue:value]]; + } else if ([token isKindOfClass:FBDescendantPredicateToken.class]) { + [predicates addObject:[[FBDescendantPredicateItem alloc] initWithValue:value]]; + } + } else if ([token isKindOfClass:FBNumberToken.class]) { + if (isPositionSet) { + NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be set only once.", token.asString]; + if (error) { + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + NSNumber *position = [numberFormatter numberFromString:token.asString]; + if (nil == position || 0 == position.intValue) { + NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be a valid integer number not equal to zero.", token.asString]; + if (error) { + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + chainElementPosition = position; + isPositionSet = YES; + } else if ([token isKindOfClass:FBSplitterToken.class]) { + if (!isPositionSet) { + chainElementPosition = nil; + } + if (isDescendantSet) { + if (isTypeSet) { + [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]]; + isDescendantSet = NO; + } + } else { + [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]]; + } + isTypeSet = NO; + isPositionSet = NO; + [predicates removeAllObjects]; + } + } + if (!isPositionSet) { + chainElementPosition = nil; + } + if (isDescendantSet) { + if (isTypeSet) { + [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]]; + } else { + if (error) { + NSString *description = @"Descendants lookup modifier '**/' should be followed with the actual element type"; + *error = [self.class compilationErrorWithQuery:originalQuery description:description]; + } + return nil; + } + } else { + [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]]; + } + return [[FBClassChain alloc] initWithElements:result.copy]; +} + ++ (FBClassChain *)parseQuery:(NSString*)classChainQuery error:(NSError **)error +{ + NSAssert(classChainQuery.length > 0, @"Query length should be greater than zero", nil); + NSArray *tokenizedQuery = [self.class tokenizedQueryWithQuery:classChainQuery error:error]; + if (nil == tokenizedQuery) { + return nil; + } + return [self.class compiledQueryWithTokenizedQuery:tokenizedQuery originalQuery:classChainQuery error:error]; +} + +@end + + +@implementation FBAbstractPredicateItem + +- (instancetype)initWithValue:(NSPredicate *)value +{ + self = [super init]; + if (self) { + _value = value; + } + return self; +} + +@end + +@implementation FBSelfPredicateItem + +@end + +@implementation FBDescendantPredicateItem + +@end diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h new file mode 100644 index 0000000..f36117e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -0,0 +1,375 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const FBSnapshotMaxDepthKey; + +/** + Accessors for Global Constants. + */ +@interface FBConfiguration : NSObject + +/*! If set to YES will ask TestManagerDaemon for element visibility */ ++ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value; ++ (BOOL)shouldUseTestManagerForVisibilityDetection; + +/*! If set to YES will use compact (standards-compliant) & faster responses */ ++ (void)setShouldUseCompactResponses:(BOOL)value; ++ (BOOL)shouldUseCompactResponses; + +/*! If set to YES (which is the default), the app will be terminated at the end of the session, if a bundleId was specified */ ++ (void)setShouldTerminateApp:(BOOL)value; ++ (BOOL)shouldTerminateApp; + +/*! If shouldUseCompactResponses == NO, is the comma-separated list of fields to return with each element. Defaults to "type,label". */ ++ (void)setElementResponseAttributes:(NSString *)value; ++ (NSString *)elementResponseAttributes; + +/*! Disables remote query evaluation making Xcode 9.x tests behave same as Xcode 8.x test */ ++ (void)disableRemoteQueryEvaluation; + +/*! Enables the extended XCTest debug logging. Useful for developemnt purposes */ ++ (void)enableXcTestDebugLogs; + +/*! Disables attribute key path analysis, which will cause XCTest on Xcode 9.x to ignore some elements */ ++ (void)disableAttributeKeyPathAnalysis; + +/*! Disables XCTest automated screenshots taking */ ++ (void)disableScreenshots; +/*! Enables XCTest automated screenshots taking */ ++ (void)enableScreenshots; + +/*! Disables XCTest automated videos taking (iOS 17+) */ ++ (void)disableScreenRecordings; +/*! Enables XCTest automated videos taking (iOS 17+) */ ++ (void)enableScreenRecordings; + +/* The maximum typing frequency for all typing activities */ ++ (void)setMaxTypingFrequency:(NSUInteger)value; ++ (NSUInteger)maxTypingFrequency; ++ (NSUInteger)defaultTypingFrequency; + +/* Use singleton test manager proxy */ ++ (void)setShouldUseSingletonTestManager:(BOOL)value; ++ (BOOL)shouldUseSingletonTestManager; + +/* Enforces WDA to verify the presense of system alerts while checking for an active app */ ++ (void)setShouldRespectSystemAlerts:(BOOL)value; ++ (BOOL)shouldRespectSystemAlerts; + +/** + * Extract switch value from arguments + * + * @param arguments Array of strings with the command-line arguments, e.g. @[@"--port", @"12345"]. + * @param key Switch to look up value for, e.g. @"--port". + * + * @return Switch value or nil if the switch is not present in arguments. + */ ++ (NSString* _Nullable)valueFromArguments: (NSArray *)arguments forKey: (NSString*)key; + +/** + The quality of the screenshots generated by the screenshots broadcaster, expressed + as a value from 0 to 100. The value 0 represents the maximum compression + (or lowest quality) while the value 100 represents the least compression (or best + quality). The default value is 25. + */ ++ (NSUInteger)mjpegServerScreenshotQuality; ++ (void)setMjpegServerScreenshotQuality:(NSUInteger)quality; + +/** + Whether to apply orientation fixes to the streamed JPEG images. + This is an expensive operation and it is disabled by default, so screenshots + are returned in portrait, but their actual orientation value could still be found in the EXIF + metadata. + ! Enablement of this setting may lead to WDA process termination because of an excessive CPU usage. + */ ++ (BOOL)mjpegShouldFixOrientation; ++ (void)setMjpegShouldFixOrientation:(BOOL)enabled; + +/** + The framerate at which the background screenshots broadcaster should broadcast + screenshots in range 1..60. The default value is 10 (Frames Per Second). + Setting zero value will cause the framerate to be at its maximum possible value. + */ ++ (NSUInteger)mjpegServerFramerate; ++ (void)setMjpegServerFramerate:(NSUInteger)framerate; + +/** + Whether to limit the XPath scope to descendant items only while performing a lookup + in an element context. Enabled by default. Being disabled, allows to use XPath locators + like ".." in order to match parent items of the current context root. + */ ++ (BOOL)limitXpathContextScope; ++ (void)setLimitXpathContextScope:(BOOL)enabled; + +/** + The quality of display screenshots. The higher quality you set is the bigger screenshot size is. + The highest quality value is 0 (lossless PNG) or 3 (lossless HEIC). The lowest quality is 2 (highly compressed JPEG). + The default quality value is 3 (lossless HEIC). + See https://developer.apple.com/documentation/xctest/xctimagequality?language=objc + */ ++ (NSUInteger)screenshotQuality; ++ (void)setScreenshotQuality:(NSUInteger)quality; + +/** + The range of ports that the HTTP Server should attempt to bind on launch + */ ++ (NSRange)bindingPortRange; + +/** + The port number where the background screenshots broadcaster is supposed to run + */ ++ (NSInteger)mjpegServerPort; + +/** + The scaling factor for frames of the mjpeg stream. The default (and maximum) value is 100, + which does not perform any scaling. The minimum value must be greater than zero. + ! Setting this to a value less than 100, especially together with orientation fixing enabled + ! may lead to WDA process termination because of an excessive CPU usage. + */ ++ (CGFloat)mjpegScalingFactor; ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor; + +/** + YES if verbose logging is enabled. NO otherwise. + */ ++ (BOOL)verboseLoggingEnabled; + +/** + Disables automatic handling of XCTest UI interruptions. + */ ++ (void)disableApplicationUIInterruptionsHandling; + +/** + * Configure keyboards preference to make test running stable + */ ++ (void)configureDefaultKeyboardPreferences; + + +/** + * Turn on softwar keyboard forcefully for simulator. + */ ++ (void)forceSimulatorSoftwareKeyboardPresence; + +/** +Defines keyboard preference enabled status +*/ +typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + FBConfigurationKeyboardPreferenceDisabled = 0, + FBConfigurationKeyboardPreferenceEnabled = 1, + FBConfigurationKeyboardPreferenceNotSupported = 2, +}; + +/** + * Modify keyboard configuration of 'auto-correction'. + * + * @param isEnabled Turn the configuration on if the value is YES + */ ++ (void)setKeyboardAutocorrection:(BOOL)isEnabled; ++ (FBConfigurationKeyboardPreference)keyboardAutocorrection; + +/** + * Modify keyboard configuration of 'predictive' + * + * @param isEnabled Turn the configuration on if the value is YES + */ ++ (void)setKeyboardPrediction:(BOOL)isEnabled; ++ (FBConfigurationKeyboardPreference)keyboardPrediction; + +/** + Sets maximum depth for traversing elements tree from parents to children while requesting XCElementSnapshot. + Used to set maxDepth value in a dictionary provided by XCAXClient_iOS's method defaultParams. + The original XCAXClient_iOS maxDepth value is set to INT_MAX, which is too big for some queries + (for example: searching elements inside a WebView). + Reasonable values are from 15 to 100 (larger numbers make queries slower). + + @param maxDepth The number of maximum depth for traversing elements tree + */ ++ (void)setSnapshotMaxDepth:(int)maxDepth; + +/** + @return The number of maximum depth for traversing elements tree + */ ++ (int)snapshotMaxDepth; + +/** + * Whether to use fast search result matching while searching for elements. + * By default this is disabled due to https://github.com/appium/appium/issues/10101 + * but it still makes sense to enable it for views containing large counts of elements + * + * @param enabled Either YES or NO + */ ++ (void)setUseFirstMatch:(BOOL)enabled; ++ (BOOL)useFirstMatch; + +/** + * Whether to bound the lookup results by index. + * By default this is disabled and bounding by accessibility is used. + * Read https://stackoverflow.com/questions/49307513/meaning-of-allelementsboundbyaccessibilityelement + * for more details on these two bounding methods. + * + * @param enabled Either YES or NO + */ ++ (void)setBoundElementsByIndex:(BOOL)enabled; ++ (BOOL)boundElementsByIndex; + +/** + * Modify reduce motion configuration in accessibility. + * It works only for Simulator since Real device has security model which allows chnaging preferences + * only from settings app. + * + * @param isEnabled Turn the configuration on if the value is YES + */ ++ (void)setReduceMotionEnabled:(BOOL)isEnabled; ++ (BOOL)reduceMotionEnabled; + +/** + * Set the idling timeout. If the timeout expires then WDA + * tries to interact with the application even if it is not idling. + * Setting it to zero disables idling checks. + * The default timeout is set to 10 seconds. + * + * @param timeout The actual timeout value in float seconds + */ ++ (void)setWaitForIdleTimeout:(NSTimeInterval)timeout; ++ (NSTimeInterval)waitForIdleTimeout; + +/** + * Set the idling timeout for different actions, for example events synthesis, rotation change, + * etc. If the timeout expires then WDA tries to interact with the application even if it is not idling. + * Setting it to zero disables idling checks. + * The default timeout is set to 2 seconds. + * + * @param timeout The actual timeout value in float seconds + */ ++ (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout; ++ (NSTimeInterval)animationCoolOffTimeout; + +/** + Enforces the page hierarchy to include non modal elements, + like Contacts. By default such elements are not present there. + See https://github.com/appium/appium/issues/13227 + + @param isEnabled Set to YES in order to enable non modal elements inclusion. + Setting this value to YES will have no effect if the current iOS SDK does not support such feature. + */ ++ (void)setIncludeNonModalElements:(BOOL)isEnabled; ++ (BOOL)includeNonModalElements; + +/** + Sets custom class chain locators for accept/dismiss alert buttons location. + This might be useful if the default buttons detection algorithm fails to determine alert buttons properly + when defaultAlertAction is set. + + @param classChainSelector Valid class chain locator, which determines accept/reject button + on the alert. The search root is the alert element itself. + Setting this value to nil or an empty string (the default + value) will enforce WDA to apply the default algorithm for alert buttons location. + If an invalid/non-parseable locator is set then the lookup will fallback to the default algorithm and print a + warning into the log. + Example: ** /XCUIElementTypeButton[`label CONTAINS[c] 'accept'`] + */ ++ (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector; ++ (NSString *)acceptAlertButtonSelector; ++ (void)setDismissAlertButtonSelector:(NSString *)classChainSelector; ++ (NSString *)dismissAlertButtonSelector; + +/** + Sets class chain selector to apply for an automated alert click + */ ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector; ++ (NSString *)autoClickAlertSelector; + +/** + * Whether to use HIDEvent for text clear. + * By default this is enabled and HIDEvent is used for text clear. + * + * @param enabled Either YES or NO + */ ++ (void)setUseClearTextShortcut:(BOOL)enabled; ++ (BOOL)useClearTextShortcut; + +#if !TARGET_OS_TV +/** + Set the screenshot orientation for iOS + + It helps to fix the screenshot orientation when the device under test's orientation changes. + For example, when a device changes to the landscape, the screenshot orientation could be wrong. + Then, this setting can force change the screenshot orientation. + Xcode versions, OS versions or device models and simulator or real device could influence it. + + @param orientation Set the orientation to adjust the screenshot. + Case insensitive "portrait", "portraitUpsideDown", "landscapeRight" and "landscapeLeft" are available + to force the coodinate adjust. Other words are handled as "auto", which handles + the adjustment automatically. Defaults to "auto". + @param error If no availale orientation strategy was given, it returns an NSError object that describes the problem. + */ ++ (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error; + +/** +@return The value of UIInterfaceOrientation +*/ ++ (NSInteger)screenshotOrientation; + +/** +@return The orientation as String for human read +*/ ++ (NSString *)humanReadableScreenshotOrientation; + +#endif + +/** + Resets all session-specific settings to their default values + */ ++ (void)resetSessionSettings; + +/** + * Whether to calculate `hittable` attribute using native APIs + * instead of legacy heuristics. + * This flag improves accuracy, but may affect performance. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeHittableInPageSource:(BOOL)enabled; ++ (BOOL)includeHittableInPageSource; + +/** + * Whether to include `nativeFrame` attribute in the XML page source. + * + * When enabled, the XML representation will contain the precise rendered + * frame of the UI element. + * + * This value is more accurate than the legacy `wdFrame`, which applies rounding + * and may introduce inconsistencies in size and position calculations. + * + * The value is disabled by default to avoid potential performance overhead. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled; ++ (BOOL)includeNativeFrameInPageSource; + +/** + * Whether to include `minValue`/`maxValue` attributes in the page source. + * These attributes are retrieved from native element snapshots and represent + * value boundaries for elements like sliders or progress indicators. + * This may affect performance if used on many elements. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled; ++ (BOOL)includeMinMaxValueInPageSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m new file mode 100644 index 0000000..5cd9947 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -0,0 +1,677 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBConfiguration.h" + +#import "AXSettings.h" +#import "UIKeyboardImpl.h" +#import "TIPreferencesController.h" + +#include +#import + +#include "TargetConditionals.h" +#import "FBXCodeCompatibility.h" +#import "XCAXClient_iOS+FBSnapshotReqParams.h" +#import "XCTestPrivateSymbols.h" +#import "XCTestConfiguration.h" +#import "XCUIApplication+FBUIInterruptions.h" + +static NSUInteger const DefaultStartingPort = 8567; +static NSUInteger const DefaultMjpegServerPort = 9567; +static NSUInteger const DefaultPortRange = 100; + +static char const *const controllerPrefBundlePath = "/System/Library/PrivateFrameworks/TextInput.framework/TextInput"; +static NSString *const controllerClassName = @"TIPreferencesController"; +static NSString *const FBKeyboardAutocorrectionKey = @"KeyboardAutocorrection"; +static NSString *const FBKeyboardPredictionKey = @"KeyboardPrediction"; +static NSString *const axSettingsClassName = @"AXSettings"; + +static BOOL FBShouldUseTestManagerForVisibilityDetection = NO; +static BOOL FBShouldUseSingletonTestManager = YES; +static BOOL FBShouldRespectSystemAlerts = NO; + +static CGFloat FBMjpegScalingFactor = 100.0; +static BOOL FBMjpegShouldFixOrientation = NO; +static NSUInteger FBMjpegServerScreenshotQuality = 25; +static NSUInteger FBMjpegServerFramerate = 10; + +// Session-specific settings +static BOOL FBShouldTerminateApp; +static NSNumber* FBMaxTypingFrequency; +static NSUInteger FBScreenshotQuality; +static BOOL FBShouldUseFirstMatch; +static BOOL FBShouldBoundElementsByIndex; +static BOOL FBIncludeNonModalElements; +static NSString *FBAcceptAlertButtonSelector; +static NSString *FBDismissAlertButtonSelector; +static NSString *FBAutoClickAlertSelector; +static NSTimeInterval FBWaitForIdleTimeout; +static NSTimeInterval FBAnimationCoolOffTimeout; +static BOOL FBShouldUseCompactResponses; +static NSString *FBElementResponseAttributes; +static BOOL FBUseClearTextShortcut; +static BOOL FBLimitXpathContextScope = YES; +#if !TARGET_OS_TV +static UIInterfaceOrientation FBScreenshotOrientation; +#endif +static BOOL FBShouldIncludeHittableInPageSource = NO; +static BOOL FBShouldIncludeNativeFrameInPageSource = NO; +static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; + +@implementation FBConfiguration + ++ (NSUInteger)defaultTypingFrequency +{ + NSInteger defaultFreq = [[NSUserDefaults standardUserDefaults] + integerForKey:@"com.apple.xctest.iOSMaximumTypingFrequency"]; + return defaultFreq > 0 ? defaultFreq : 60; +} + ++ (void)initialize +{ + [FBConfiguration resetSessionSettings]; +} + +#pragma mark Public + ++ (void)disableRemoteQueryEvaluation +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableRemoteQueryEvaluation"]; +} + ++ (void)disableApplicationUIInterruptionsHandling +{ + [XCUIApplication fb_disableUIInterruptionsHandling]; +} + ++ (void)enableXcTestDebugLogs +{ + ((XCTestConfiguration *)XCTestConfiguration.activeTestConfiguration).emitOSLogs = YES; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTEmitOSLogs"]; +} + ++ (void)disableAttributeKeyPathAnalysis +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableAttributeKeyPathAnalysis"]; +} + ++ (void)disableScreenshots +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableScreenshots"]; +} + ++ (void)enableScreenshots +{ + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableScreenshots"]; +} + ++ (void)disableScreenRecordings +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableDiagnosticScreenRecordings"]; +} + ++ (void)enableScreenRecordings +{ + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableDiagnosticScreenRecordings"]; +} + ++ (NSRange)bindingPortRange +{ + // 'WebDriverAgent --port 8080' can be passed via the arguments to the process + if (self.bindingPortRangeFromArguments.location != NSNotFound) { + return self.bindingPortRangeFromArguments; + } + + // Existence of USE_PORT in the environment implies the port range is managed by the launching process. + if (NSProcessInfo.processInfo.environment[@"USE_PORT"] && + [NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) { + return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1); + } + + return NSMakeRange(DefaultStartingPort, DefaultPortRange); +} + ++ (NSInteger)mjpegServerPort +{ + if (self.mjpegServerPortFromArguments != NSNotFound) { + return self.mjpegServerPortFromArguments; + } + + if (NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] && + [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] length] > 0) { + return [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] integerValue]; + } + + return DefaultMjpegServerPort; +} + ++ (CGFloat)mjpegScalingFactor +{ + return FBMjpegScalingFactor; +} + ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor { + FBMjpegScalingFactor = scalingFactor; +} + ++ (BOOL)mjpegShouldFixOrientation +{ + return FBMjpegShouldFixOrientation; +} + ++ (void)setMjpegShouldFixOrientation:(BOOL)enabled { + FBMjpegShouldFixOrientation = enabled; +} + ++ (BOOL)verboseLoggingEnabled +{ + return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue]; +} + ++ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value +{ + FBShouldUseTestManagerForVisibilityDetection = value; +} + ++ (BOOL)shouldUseTestManagerForVisibilityDetection +{ + return FBShouldUseTestManagerForVisibilityDetection; +} + ++ (void)setShouldUseCompactResponses:(BOOL)value +{ + FBShouldUseCompactResponses = value; +} + ++ (BOOL)shouldUseCompactResponses +{ + return FBShouldUseCompactResponses; +} + ++ (void)setShouldTerminateApp:(BOOL)value +{ + FBShouldTerminateApp = value; +} + ++ (BOOL)shouldTerminateApp +{ + return FBShouldTerminateApp; +} + ++ (void)setElementResponseAttributes:(NSString *)value +{ + FBElementResponseAttributes = value; +} + ++ (NSString *)elementResponseAttributes +{ + return FBElementResponseAttributes; +} + ++ (void)setMaxTypingFrequency:(NSUInteger)value +{ + FBMaxTypingFrequency = @(value); +} + ++ (NSUInteger)maxTypingFrequency +{ + if (nil == FBMaxTypingFrequency) { + return [self defaultTypingFrequency]; + } + return FBMaxTypingFrequency.integerValue <= 0 + ? [self defaultTypingFrequency] + : FBMaxTypingFrequency.integerValue; +} + ++ (void)setShouldUseSingletonTestManager:(BOOL)value +{ + FBShouldUseSingletonTestManager = value; +} + ++ (BOOL)shouldUseSingletonTestManager +{ + return FBShouldUseSingletonTestManager; +} + ++ (NSUInteger)mjpegServerFramerate +{ + return FBMjpegServerFramerate; +} + ++ (void)setMjpegServerFramerate:(NSUInteger)framerate +{ + FBMjpegServerFramerate = framerate; +} + ++ (NSUInteger)mjpegServerScreenshotQuality +{ + return FBMjpegServerScreenshotQuality; +} + ++ (void)setMjpegServerScreenshotQuality:(NSUInteger)quality +{ + FBMjpegServerScreenshotQuality = quality; +} + ++ (NSUInteger)screenshotQuality +{ + return FBScreenshotQuality; +} + ++ (void)setScreenshotQuality:(NSUInteger)quality +{ + FBScreenshotQuality = quality; +} + ++ (NSTimeInterval)waitForIdleTimeout +{ + return FBWaitForIdleTimeout; +} + ++ (void)setWaitForIdleTimeout:(NSTimeInterval)timeout +{ + FBWaitForIdleTimeout = timeout; +} + ++ (NSTimeInterval)animationCoolOffTimeout +{ + return FBAnimationCoolOffTimeout; +} + ++ (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout +{ + FBAnimationCoolOffTimeout = timeout; +} + +// Works for Simulator and Real devices ++ (void)configureDefaultKeyboardPreferences +{ + void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY); + + Class controllerClass = NSClassFromString(controllerClassName); + + TIPreferencesController *controller = [controllerClass sharedPreferencesController]; + // Auto-Correction in Keyboards + // 'setAutocorrectionEnabled' Was in TextInput.framework/TIKeyboardState.h over iOS 10.3 + if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) { + // Under iOS 10.2 + controller.autocorrectionEnabled = NO; + } else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { + // Over iOS 10.3 + [controller setValue:@NO forPreferenceKey:FBKeyboardAutocorrectionKey]; + } + + // Predictive in Keyboards + if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) { + controller.predictionEnabled = NO; + } else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { + [controller setValue:@NO forPreferenceKey:FBKeyboardPredictionKey]; + } + + // To dismiss keyboard tutorial on iOS 11+ (iPad) + if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { + [controller setValue:@YES forPreferenceKey:@"DidShowGestureKeyboardIntroduction"]; + if (isSDKVersionGreaterThanOrEqualTo(@"13.0")) { + [controller setValue:@YES forPreferenceKey:@"DidShowContinuousPathIntroduction"]; + } + [controller synchronizePreferences]; + } + + dlclose(handle); +} + ++ (void)forceSimulatorSoftwareKeyboardPresence +{ +#if TARGET_OS_SIMULATOR + // Force toggle software keyboard on. + // This can avoid 'Keyboard is not present' error which can happen + // when send_keys are called by client + [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; + + if ([(NSObject *)[UIKeyboardImpl sharedInstance] + respondsToSelector:@selector(setSoftwareKeyboardShownByTouch:)]) { + // Xcode 13 no longer has this method + [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; + } +#endif +} + ++ (FBConfigurationKeyboardPreference)keyboardAutocorrection +{ + return [self keyboardsPreference:FBKeyboardAutocorrectionKey]; +} + ++ (void)setKeyboardAutocorrection:(BOOL)isEnabled +{ + [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardAutocorrectionKey]; +} + ++ (FBConfigurationKeyboardPreference)keyboardPrediction +{ + return [self keyboardsPreference:FBKeyboardPredictionKey]; +} + ++ (void)setKeyboardPrediction:(BOOL)isEnabled +{ + [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardPredictionKey]; +} + ++ (void)setSnapshotMaxDepth:(int)maxDepth +{ + FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth)); +} + ++ (int)snapshotMaxDepth +{ + return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey) intValue]; +} + ++ (void)setShouldRespectSystemAlerts:(BOOL)value +{ + FBShouldRespectSystemAlerts = value; +} + ++ (BOOL)shouldRespectSystemAlerts +{ + return FBShouldRespectSystemAlerts; +} + ++ (void)setUseFirstMatch:(BOOL)enabled +{ + FBShouldUseFirstMatch = enabled; +} + ++ (BOOL)useFirstMatch +{ + return FBShouldUseFirstMatch; +} + ++ (void)setBoundElementsByIndex:(BOOL)enabled +{ + FBShouldBoundElementsByIndex = enabled; +} + ++ (BOOL)boundElementsByIndex +{ + return FBShouldBoundElementsByIndex; +} + ++ (void)setIncludeNonModalElements:(BOOL)isEnabled +{ + FBIncludeNonModalElements = isEnabled; +} + ++ (BOOL)includeNonModalElements +{ + return FBIncludeNonModalElements; +} + ++ (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector +{ + FBAcceptAlertButtonSelector = classChainSelector; +} + ++ (NSString *)acceptAlertButtonSelector +{ + return FBAcceptAlertButtonSelector; +} + ++ (void)setDismissAlertButtonSelector:(NSString *)classChainSelector +{ + FBDismissAlertButtonSelector = classChainSelector; +} + ++ (NSString *)dismissAlertButtonSelector +{ + return FBDismissAlertButtonSelector; +} + ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector +{ + FBAutoClickAlertSelector = classChainSelector; +} + ++ (NSString *)autoClickAlertSelector +{ + return FBAutoClickAlertSelector; +} + ++ (void)setUseClearTextShortcut:(BOOL)enabled +{ + FBUseClearTextShortcut = enabled; +} + ++ (BOOL)useClearTextShortcut +{ + return FBUseClearTextShortcut; +} + ++ (BOOL)limitXpathContextScope +{ + return FBLimitXpathContextScope; +} + ++ (void)setLimitXpathContextScope:(BOOL)enabled +{ + FBLimitXpathContextScope = enabled; +} + +#if !TARGET_OS_TV ++ (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error +{ + // Only UIInterfaceOrientationUnknown is over iOS 8. Others are over iOS 2. + // https://developer.apple.com/documentation/uikit/uiinterfaceorientation/uiinterfaceorientationunknown + if ([orientation.lowercaseString isEqualToString:@"portrait"]) { + FBScreenshotOrientation = UIInterfaceOrientationPortrait; + } else if ([orientation.lowercaseString isEqualToString:@"portraitupsidedown"]) { + FBScreenshotOrientation = UIInterfaceOrientationPortraitUpsideDown; + } else if ([orientation.lowercaseString isEqualToString:@"landscaperight"]) { + FBScreenshotOrientation = UIInterfaceOrientationLandscapeRight; + } else if ([orientation.lowercaseString isEqualToString:@"landscapeleft"]) { + FBScreenshotOrientation = UIInterfaceOrientationLandscapeLeft; + } else if ([orientation.lowercaseString isEqualToString:@"auto"]) { + FBScreenshotOrientation = UIInterfaceOrientationUnknown; + } else { + return [[FBErrorBuilder.builder withDescriptionFormat: + @"The orientation value '%@' is not known. Only the following orientation values are supported: " \ + "'auto', 'portrait', 'portraitUpsideDown', 'landscapeRight' and 'landscapeLeft'", orientation] + buildError:error]; + } + return YES; +} + ++ (NSInteger)screenshotOrientation +{ + return FBScreenshotOrientation; +} + ++ (NSString *)humanReadableScreenshotOrientation +{ + switch (FBScreenshotOrientation) { + case UIInterfaceOrientationPortrait: + return @"portrait"; + case UIInterfaceOrientationPortraitUpsideDown: + return @"portraitUpsideDown"; + case UIInterfaceOrientationLandscapeRight: + return @"landscapeRight"; + case UIInterfaceOrientationLandscapeLeft: + return @"landscapeLeft"; + case UIInterfaceOrientationUnknown: + return @"auto"; + } +} +#endif + ++ (void)resetSessionSettings +{ + FBShouldTerminateApp = YES; + FBShouldUseCompactResponses = YES; + FBElementResponseAttributes = @"type,label"; + FBMaxTypingFrequency = @([self defaultTypingFrequency]); + FBScreenshotQuality = 3; + FBShouldUseFirstMatch = NO; + FBShouldBoundElementsByIndex = NO; + // This is diabled by default because enabling it prevents the accessbility snapshot to be taken + // (it always errors with kxIllegalArgument error) + FBIncludeNonModalElements = NO; + FBAcceptAlertButtonSelector = @""; + FBDismissAlertButtonSelector = @""; + FBAutoClickAlertSelector = @""; + FBWaitForIdleTimeout = 10.; + FBAnimationCoolOffTimeout = 2.; + // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100. + FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50); + FBUseClearTextShortcut = YES; + FBLimitXpathContextScope = YES; +#if !TARGET_OS_TV + FBScreenshotOrientation = UIInterfaceOrientationUnknown; +#endif +} + +#pragma mark Private + ++ (FBConfigurationKeyboardPreference)keyboardsPreference:(nonnull NSString *)key +{ + Class controllerClass = NSClassFromString(controllerClassName); + TIPreferencesController *controller = [controllerClass sharedPreferencesController]; + if ([key isEqualToString:FBKeyboardAutocorrectionKey]) { + if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) { + return [controller boolForPreferenceKey:FBKeyboardAutocorrectionKey] + ? FBConfigurationKeyboardPreferenceEnabled + : FBConfigurationKeyboardPreferenceDisabled; + } else { + [FBLogger log:@"Updating keyboard autocorrection preference is not supported"]; + return FBConfigurationKeyboardPreferenceNotSupported; + } + } else if ([key isEqualToString:FBKeyboardPredictionKey]) { + if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) { + return [controller boolForPreferenceKey:FBKeyboardPredictionKey] + ? FBConfigurationKeyboardPreferenceEnabled + : FBConfigurationKeyboardPreferenceDisabled; + } else { + [FBLogger log:@"Updating keyboard prediction preference is not supported"]; + return FBConfigurationKeyboardPreferenceNotSupported; + } + } + @throw [[FBErrorBuilder.builder withDescriptionFormat:@"No available keyboardsPreferenceKey: '%@'", key] build]; +} + ++ (void)configureKeyboardsPreference:(BOOL)enable forPreferenceKey:(nonnull NSString *)key +{ + void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY); + Class controllerClass = NSClassFromString(controllerClassName); + + TIPreferencesController *controller = [controllerClass sharedPreferencesController]; + + if ([key isEqualToString:FBKeyboardAutocorrectionKey]) { + // Auto-Correction in Keyboards + if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) { + controller.autocorrectionEnabled = enable; + } else { + [controller setValue:@(enable) forPreferenceKey:FBKeyboardAutocorrectionKey]; + } + } else if ([key isEqualToString:FBKeyboardPredictionKey]) { + // Predictive in Keyboards + if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) { + controller.predictionEnabled = enable; + } else { + [controller setValue:@(enable) forPreferenceKey:FBKeyboardPredictionKey]; + } + } + + [controller synchronizePreferences]; + dlclose(handle); +} + ++ (NSString*)valueFromArguments: (NSArray *)arguments forKey: (NSString*)key +{ + NSUInteger index = [arguments indexOfObject:key]; + if (index == NSNotFound || index == arguments.count - 1) { + return nil; + } + return arguments[index + 1]; +} + ++ (NSUInteger)mjpegServerPortFromArguments +{ + NSString *portNumberString = [self valueFromArguments: NSProcessInfo.processInfo.arguments + forKey: @"--mjpeg-server-port"]; + NSUInteger port = (NSUInteger)[portNumberString integerValue]; + if (port == 0) { + return NSNotFound; + } + return port; +} + ++ (NSRange)bindingPortRangeFromArguments +{ + NSString *portNumberString = [self valueFromArguments:NSProcessInfo.processInfo.arguments + forKey: @"--port"]; + NSUInteger port = (NSUInteger)[portNumberString integerValue]; + if (port == 0) { + return NSMakeRange(NSNotFound, 0); + } + return NSMakeRange(port, 1); +} + ++ (void)setReduceMotionEnabled:(BOOL)isEnabled +{ + Class settingsClass = NSClassFromString(axSettingsClassName); + AXSettings *settings = [settingsClass sharedInstance]; + + // Below does not work on real devices because of iOS security model + // (lldb) po settings.reduceMotionEnabled = isEnabled + // 2019-08-21 22:58:19.776165+0900 WebDriverAgentRunner-Runner[322:13361] [User Defaults] Couldn't write value for key ReduceMotionEnabled in CFPrefsPlistSource<0x28111a700> (Domain: com.apple.Accessibility, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): setting preferences outside an application's container requires user-preference-write or file-write-data sandbox access + if ([settings respondsToSelector:@selector(setReduceMotionEnabled:)]) { + [settings setReduceMotionEnabled:isEnabled]; + } +} + ++ (BOOL)reduceMotionEnabled +{ + Class settingsClass = NSClassFromString(axSettingsClassName); + AXSettings *settings = [settingsClass sharedInstance]; + + if ([settings respondsToSelector:@selector(reduceMotionEnabled)]) { + return settings.reduceMotionEnabled; + } + return NO; +} + ++ (void)setIncludeHittableInPageSource:(BOOL)enabled +{ + FBShouldIncludeHittableInPageSource = enabled; +} + ++ (BOOL)includeHittableInPageSource +{ + return FBShouldIncludeHittableInPageSource; +} + ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled +{ + FBShouldIncludeNativeFrameInPageSource = enabled; +} + ++ (BOOL)includeNativeFrameInPageSource +{ + return FBShouldIncludeNativeFrameInPageSource; +} + ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled +{ + FBShouldIncludeMinMaxValueInPageSource = enabled; +} + ++ (BOOL)includeMinMaxValueInPageSource +{ + return FBShouldIncludeMinMaxValueInPageSource; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h new file mode 100644 index 0000000..02ebf89 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + XCTestLogger decorator that will print all debug information to console + */ +@interface FBDebugLogDelegateDecorator : NSObject + +/** + Decorates XCTestLogger by also printing debug message to console + */ ++ (void)decorateXCTestLogger; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m new file mode 100644 index 0000000..b4c2c53 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBDebugLogDelegateDecorator.h" + +#import "FBLogger.h" +#import "XCTestPrivateSymbols.h" + +@interface FBDebugLogDelegateDecorator () +@property (nonatomic, strong) id debugLogger; +@end + +@implementation FBDebugLogDelegateDecorator + ++ (void)decorateXCTestLogger +{ + FBDebugLogDelegateDecorator *decorator = [FBDebugLogDelegateDecorator new]; + id debugLogger = XCDebugLogger(); + if ([debugLogger isKindOfClass:FBDebugLogDelegateDecorator.class]) { + // Already decorated + return; + } + decorator.debugLogger = debugLogger; + XCSetDebugLogger(decorator); +} + +- (void)logDebugMessage:(NSString *)logEntry +{ + NSString *debugLogEntry = logEntry; + static NSString *processName; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + processName = [NSProcessInfo processInfo].processName; + }); + if ([logEntry rangeOfString:[NSString stringWithFormat:@" %@[", processName]].location != NSNotFound) { + // Ignoring "13:37:07.638 TestingApp[56374:10997466] " from log entry + NSUInteger ignoreCharCount = [logEntry rangeOfString:@"]"].location + 2; + debugLogEntry = [logEntry substringWithRange:NSMakeRange(ignoreCharCount, logEntry.length - ignoreCharCount)]; + } + [FBLogger verboseLog:debugLogEntry]; + [self.debugLogger logDebugMessage:logEntry]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBElementHelpers.h b/WebDriverAgentLib/Utilities/FBElementHelpers.h new file mode 100644 index 0000000..d0cf1b3 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementHelpers.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Checks if the element is a text field + + @param elementType XCTest element type + @return YES if the element is a text field + */ +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType); + +/** + Checks if the element supports min/max value attributes + + @param elementType XCTest element type + @return YES if the element type supports min/max value attributes + */ +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBElementHelpers.m b/WebDriverAgentLib/Utilities/FBElementHelpers.m new file mode 100644 index 0000000..6b9d340 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementHelpers.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementHelpers.h" + +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType) { + return elementType == XCUIElementTypeTextView + || elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSearchField + || elementType == XCUIElementTypeSecureTextField; +} + +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType) { + return elementType == XCUIElementTypeSlider + || elementType == XCUIElementTypeStepper; +} diff --git a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h new file mode 100644 index 0000000..a94ff62 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Class used to translate between XCUIElementType and string name + */ +@interface FBElementTypeTransformer : NSObject + +/** + Converts string to XCUIElementType + + @param typeName converted string to XCUIElementType + @return Proper XCUIElementType or XCUIElementTypeAny if typeName is nil or unrecognised + */ ++ (XCUIElementType)elementTypeWithTypeName:(NSString *__nullable)typeName; + +/** + Converts XCUIElementType to string + + @param type converted XCUIElementType to string + @return XCUIElementType as NSString + */ ++ (NSString *)stringWithElementType:(XCUIElementType)type; + +/** + Converts XCUIElementType to short string by striping `XCUIElementType` from it + + @param type converted XCUIElementType to string + @return XCUIElementType as NSString with stripped `XCUIElementType` + */ ++ (NSString *)shortStringWithElementType:(XCUIElementType)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m new file mode 100644 index 0000000..d3748a4 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBElementTypeTransformer.h" + +#import "FBExceptions.h" + +@implementation FBElementTypeTransformer + +static NSDictionary *ElementTypeToStringMapping; +static NSDictionary *StringToElementTypeMapping; + +static NSString const *FB_ELEMENT_TYPE_PREFIX = @"XCUIElementType"; + ++ (void)createMapping +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ElementTypeToStringMapping = + @{ + @0 : @"XCUIElementTypeAny", + @1 : @"XCUIElementTypeOther", + @2 : @"XCUIElementTypeApplication", + @3 : @"XCUIElementTypeGroup", + @4 : @"XCUIElementTypeWindow", + @5 : @"XCUIElementTypeSheet", + @6 : @"XCUIElementTypeDrawer", + @7 : @"XCUIElementTypeAlert", + @8 : @"XCUIElementTypeDialog", + @9 : @"XCUIElementTypeButton", + @10 : @"XCUIElementTypeRadioButton", + @11 : @"XCUIElementTypeRadioGroup", + @12 : @"XCUIElementTypeCheckBox", + @13 : @"XCUIElementTypeDisclosureTriangle", + @14 : @"XCUIElementTypePopUpButton", + @15 : @"XCUIElementTypeComboBox", + @16 : @"XCUIElementTypeMenuButton", + @17 : @"XCUIElementTypeToolbarButton", + @18 : @"XCUIElementTypePopover", + @19 : @"XCUIElementTypeKeyboard", + @20 : @"XCUIElementTypeKey", + @21 : @"XCUIElementTypeNavigationBar", + @22 : @"XCUIElementTypeTabBar", + @23 : @"XCUIElementTypeTabGroup", + @24 : @"XCUIElementTypeToolbar", + @25 : @"XCUIElementTypeStatusBar", + @26 : @"XCUIElementTypeTable", + @27 : @"XCUIElementTypeTableRow", + @28 : @"XCUIElementTypeTableColumn", + @29 : @"XCUIElementTypeOutline", + @30 : @"XCUIElementTypeOutlineRow", + @31 : @"XCUIElementTypeBrowser", + @32 : @"XCUIElementTypeCollectionView", + @33 : @"XCUIElementTypeSlider", + @34 : @"XCUIElementTypePageIndicator", + @35 : @"XCUIElementTypeProgressIndicator", + @36 : @"XCUIElementTypeActivityIndicator", + @37 : @"XCUIElementTypeSegmentedControl", + @38 : @"XCUIElementTypePicker", + @39 : @"XCUIElementTypePickerWheel", + @40 : @"XCUIElementTypeSwitch", + @41 : @"XCUIElementTypeToggle", + @42 : @"XCUIElementTypeLink", + @43 : @"XCUIElementTypeImage", + @44 : @"XCUIElementTypeIcon", + @45 : @"XCUIElementTypeSearchField", + @46 : @"XCUIElementTypeScrollView", + @47 : @"XCUIElementTypeScrollBar", + @48 : @"XCUIElementTypeStaticText", + @49 : @"XCUIElementTypeTextField", + @50 : @"XCUIElementTypeSecureTextField", + @51 : @"XCUIElementTypeDatePicker", + @52 : @"XCUIElementTypeTextView", + @53 : @"XCUIElementTypeMenu", + @54 : @"XCUIElementTypeMenuItem", + @55 : @"XCUIElementTypeMenuBar", + @56 : @"XCUIElementTypeMenuBarItem", + @57 : @"XCUIElementTypeMap", + @58 : @"XCUIElementTypeWebView", + @59 : @"XCUIElementTypeIncrementArrow", + @60 : @"XCUIElementTypeDecrementArrow", + @61 : @"XCUIElementTypeTimeline", + @62 : @"XCUIElementTypeRatingIndicator", + @63 : @"XCUIElementTypeValueIndicator", + @64 : @"XCUIElementTypeSplitGroup", + @65 : @"XCUIElementTypeSplitter", + @66 : @"XCUIElementTypeRelevanceIndicator", + @67 : @"XCUIElementTypeColorWell", + @68 : @"XCUIElementTypeHelpTag", + @69 : @"XCUIElementTypeMatte", + @70 : @"XCUIElementTypeDockItem", + @71 : @"XCUIElementTypeRuler", + @72 : @"XCUIElementTypeRulerMarker", + @73 : @"XCUIElementTypeGrid", + @74 : @"XCUIElementTypeLevelIndicator", + @75 : @"XCUIElementTypeCell", + @76 : @"XCUIElementTypeLayoutArea", + @77 : @"XCUIElementTypeLayoutItem", + @78 : @"XCUIElementTypeHandle", + @79 : @"XCUIElementTypeStepper", + @80 : @"XCUIElementTypeTab", + @81 : @"XCUIElementTypeTouchBar", + @82 : @"XCUIElementTypeStatusItem", + // !!! This mapping should be updated if there are changes after each new XCTest release + }; + NSMutableDictionary *swappedMapping = [NSMutableDictionary dictionary]; + [ElementTypeToStringMapping enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + swappedMapping[obj] = key; + }]; + StringToElementTypeMapping = swappedMapping.copy; + }); +} + ++ (XCUIElementType)elementTypeWithTypeName:(NSString *)typeName +{ + [self createMapping]; + NSNumber *type = StringToElementTypeMapping[typeName]; + if (nil == type) { + if ([typeName hasPrefix:(NSString *)FB_ELEMENT_TYPE_PREFIX] && typeName.length > FB_ELEMENT_TYPE_PREFIX.length) { + // Consider the element type is something new and has to be added into ElementTypeToStringMapping + return XCUIElementTypeOther; + } + NSString *reason = [NSString stringWithFormat:@"Invalid argument for class used '%@'. Did you mean %@%@?", typeName, FB_ELEMENT_TYPE_PREFIX, typeName]; + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; + } + return (XCUIElementType) type.unsignedIntegerValue; +} + ++ (NSString *)stringWithElementType:(XCUIElementType)type +{ + [self createMapping]; + NSString *typeName = ElementTypeToStringMapping[@(type)]; + return nil == typeName + // Consider the type name is something new and has to be added into ElementTypeToStringMapping + ? [NSString stringWithFormat:@"%@Other", FB_ELEMENT_TYPE_PREFIX] + : typeName; +} + ++ (NSString *)shortStringWithElementType:(XCUIElementType)type +{ + return [[self stringWithElementType:type] stringByReplacingOccurrencesOfString:(NSString *)FB_ELEMENT_TYPE_PREFIX withString:@""]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBErrorBuilder.h b/WebDriverAgentLib/Utilities/FBErrorBuilder.h new file mode 100644 index 0000000..8435eba --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBErrorBuilder.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Builder used create error raised by WebDriverAgent + */ +@interface FBErrorBuilder : NSObject + +/** + Default constructor + */ ++ (instancetype)builder; + +/** + Configures description set as NSLocalizedDescriptionKey + + @param description set as NSLocalizedDescriptionKey + @return builder instance + */ +- (instancetype)withDescription:(NSString *)description; + +/** + Configures description set as NSLocalizedDescriptionKey with convenient format + + @param format of description set as NSLocalizedDescriptionKey + @return builder instance + */ +- (instancetype)withDescriptionFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); + +/** + Configures error set as NSUnderlyingErrorKey + + @param innerError used to set NSUnderlyingErrorKey + @return builder instance + */ +- (instancetype)withInnerError:(NSError *)innerError; + +/** + Builder used create error raised by WebDriverAgent + + @return built error + */ +- (NSError *)build; + +/** + Builder used create error raised by WebDriverAgent + + @param error pointer used to return built error + @return fixed NO to apply to Apple's coding conventions + */ +- (BOOL)buildError:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBErrorBuilder.m b/WebDriverAgentLib/Utilities/FBErrorBuilder.m new file mode 100644 index 0000000..3ee5d8f --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBErrorBuilder.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBErrorBuilder.h" + +static NSString *const FBWebServerErrorDomain = @"com.facebook.WebDriverAgent"; + +@interface FBErrorBuilder () +@property (nonatomic, copy) NSString *errorDescription; +@property (nonatomic, strong) NSError *innerError; +@end + +@implementation FBErrorBuilder + ++ (instancetype)builder +{ + return [FBErrorBuilder new]; +} + +- (instancetype)withDescription:(NSString *)description +{ + self.errorDescription = description; + return self; +} + +- (instancetype)withDescriptionFormat:(NSString *)format, ... +{ + va_list argList; + va_start(argList, format); + self.errorDescription = [[NSString alloc] initWithFormat:format arguments:argList]; + va_end(argList); + return self; +} + +- (instancetype)withInnerError:(NSError *)error +{ + self.innerError = error; + return self; +} + +- (BOOL)buildError:(NSError **)errorOut +{ + if (errorOut) { + *errorOut = [self build]; + } + return NO; +} + +- (NSError *)build +{ + return + [NSError errorWithDomain:FBWebServerErrorDomain + code:1 + userInfo:[self buildUserInfo] + ]; +} + +- (NSDictionary *)buildUserInfo +{ + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (self.errorDescription) { + userInfo[NSLocalizedDescriptionKey] = self.errorDescription; + } + if (self.innerError) { + userInfo[NSUnderlyingErrorKey] = self.innerError; + } + return userInfo.copy; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h new file mode 100644 index 0000000..80d8648 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Test Case that will never fail or stop from running in case of failure + */ +@interface FBFailureProofTestCase : XCTestCase +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m new file mode 100644 index 0000000..ab91fd9 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBFailureProofTestCase.h" + +#import "FBLogger.h" + +@implementation FBFailureProofTestCase + +- (void)setUp +{ + [super setUp]; + self.continueAfterFailure = YES; + // https://github.com/appium/appium/issues/13949 + self.shouldSetShouldHaltWhenReceivesControl = NO; + self.shouldHaltWhenReceivesControl = NO; +} + +- (void)_recordIssue:(XCTIssue *)issue +{ + NSString *description = [NSString stringWithFormat:@"%@ (%@)", issue.compactDescription, issue.associatedError.description]; + [FBLogger logFmt:@"Issue type: %ld", issue.type]; + [self _enqueueFailureWithDescription:description + inFile:issue.sourceCodeContext.location.fileURL.path + atLine:issue.sourceCodeContext.location.lineNumber + // 5 == XCTIssueTypeUnmatchedExpectedFailure + expected:issue.type == 5]; +} + +- (void)_recordIssue:(XCTIssue *)issue forCaughtError:(id)error +{ + [self _recordIssue:issue]; +} + +- (void)recordIssue:(XCTIssue *)issue +{ + [self _recordIssue:issue]; +} + +/** + Override 'recordFailureWithDescription' to not stop by failures. + */ +- (void)recordFailureWithDescription:(NSString *)description + inFile:(NSString *)filePath + atLine:(NSUInteger)lineNumber + expected:(BOOL)expected +{ + [self _enqueueFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +/** + Private XCTestCase method used to block and tunnel failure messages + */ +- (void)_enqueueFailureWithDescription:(NSString *)description + inFile:(NSString *)filePath + atLine:(NSUInteger)lineNumber + expected:(BOOL)expected +{ + [FBLogger logFmt:@"Enqueue Failure: %@ %@ %lu %d", description, filePath, (unsigned long)lineNumber, expected]; + // TODO: Figure out which error types we want to escalate +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.h b/WebDriverAgentLib/Utilities/FBImageProcessor.h new file mode 100644 index 0000000..e83b830 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@class UTType; + +NS_ASSUME_NONNULL_BEGIN + +// Those values define the allowed ranges for the scaling factor and compression quality settings +extern const CGFloat FBMinScalingFactor; +extern const CGFloat FBMaxScalingFactor; +extern const CGFloat FBMinCompressionQuality; +extern const CGFloat FBMaxCompressionQuality; + +@interface FBImageProcessor : NSObject + +/** + Puts the passed image on the queue and dispatches a scaling operation. If there is already a image on the + queue it will be replaced with the new one + + @param image The image to scale down + @param completionHandler called after successfully scaling down an image + @param scalingFactor the scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all + */ +- (void)submitImageData:(NSData *)image + scalingFactor:(CGFloat)scalingFactor + completionHandler:(void (^)(NSData *))completionHandler; + +/** + Scales and crops the source image + + @param image The source image data + @param uti Either UTTypePNG or UTTypeJPEG + @param scalingFactor Scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all + @param compressionQuality the compression quality in range 0.0..1.0 (0.0 for max. compression and 1.0 for lossless compression). + Only works if UTI is set to kUTTypeJPEG + @param error The actual error instance if the returned result is nil + @returns Processed image data compressed according to the given UTI or nil in case of a failure + */ +- (nullable NSData *)scaledImageWithData:(NSData *)image + uti:(UTType *)uti + scalingFactor:(CGFloat)scalingFactor + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m new file mode 100644 index 0000000..7d2ada7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBImageProcessor.h" + +#import +#import +@import UniformTypeIdentifiers; + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBImageUtils.h" +#import "FBLogger.h" + +const CGFloat FBMinScalingFactor = 0.01f; +const CGFloat FBMaxScalingFactor = 1.0f; +const CGFloat FBMinCompressionQuality = 0.0f; +const CGFloat FBMaxCompressionQuality = 1.0f; + +@interface FBImageProcessor () + +@property (nonatomic) NSData *nextImage; +@property (nonatomic, readonly) NSLock *nextImageLock; +@property (nonatomic, readonly) dispatch_queue_t scalingQueue; + +@end + +@implementation FBImageProcessor + +- (id)init +{ + self = [super init]; + if (self) { + _nextImageLock = [[NSLock alloc] init]; + _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); + } + return self; +} + +- (void)submitImageData:(NSData *)image + scalingFactor:(CGFloat)scalingFactor + completionHandler:(void (^)(NSData *))completionHandler +{ + [self.nextImageLock lock]; + if (self.nextImage != nil) { + [FBLogger verboseLog:@"Discarding screenshot"]; + } + self.nextImage = image; + [self.nextImageLock unlock]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcompletion-handler" + dispatch_async(self.scalingQueue, ^{ + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + [self.nextImageLock unlock]; + if (nextImageData == nil) { + return; + } + + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + completionHandler(thumbnailData ?: nextImageData); + }); +#pragma clang diagnostic pop +} + ++ (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData + scalingFactor:(CGFloat)scalingFactor + uti:(UTType *)uti + compressionQuality:(CGFloat)compressionQuality + fixOrientation:(BOOL)fixOrientation + desiredOrientation:(nullable NSNumber *)orientation +{ + scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor)); + BOOL usesScaling = scalingFactor > 0.0 && scalingFactor < FBMaxScalingFactor; + @autoreleasepool { + if (!usesScaling && !fixOrientation) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + + UIImage *image = [UIImage imageWithData:imageData]; + if (nil == image + || ((image.imageOrientation == UIImageOrientationUp || !fixOrientation) && !usesScaling)) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + + CGSize scaledSize = CGSizeMake(image.size.width * scalingFactor, image.size.height * scalingFactor); + if (!fixOrientation && usesScaling) { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block UIImage *result = nil; + [image prepareThumbnailOfSize:scaledSize + completionHandler:^(UIImage * _Nullable thumbnail) { + result = thumbnail; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (nil == result) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + return [uti conformsToType:UTTypePNG] + ? UIImagePNGRepresentation(result) + : UIImageJPEGRepresentation(result, compressionQuality); + } + + UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init]; + format.scale = scalingFactor; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize + format:format]; + UIImageOrientation desiredOrientation = orientation == nil + ? image.imageOrientation + : (UIImageOrientation)orientation.integerValue; + UIImage *uiImage = [UIImage imageWithCGImage:(CGImageRef)image.CGImage + scale:image.scale + orientation:desiredOrientation]; + return [uti conformsToType:UTTypePNG] + ? [renderer PNGDataWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; + }] + : [renderer JPEGDataWithCompressionQuality:compressionQuality + actions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; + }]; + } +} + +- (nullable NSData *)scaledImageWithData:(NSData *)imageData + uti:(UTType *)uti + scalingFactor:(CGFloat)scalingFactor + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error +{ + NSNumber *orientation = nil; +#if !TARGET_OS_TV + if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) { + orientation = @(UIImageOrientationUp); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) { + orientation = @(UIImageOrientationDown); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) { + orientation = @(UIImageOrientationRight); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) { + orientation = @(UIImageOrientationLeft); + } +#endif + NSData *resultData = [self.class fixedImageDataWithImageData:imageData + scalingFactor:scalingFactor + uti:uti + compressionQuality:compressionQuality + fixOrientation:YES + desiredOrientation:orientation]; + return resultData ?: imageData; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.h b/WebDriverAgentLib/Utilities/FBImageUtils.h new file mode 100644 index 0000000..0750165 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBImageUtils.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/*! Returns YES if the data contains a PNG image */ +BOOL FBIsPngImage(NSData *imageData); + +/*! Converts the given image data to a PNG representation if necessary */ +NSData *_Nullable FBToPngData(NSData *imageData); + +/*! Returns YES if the data contains a JPG image */ +BOOL FBIsJpegImage(NSData *imageData); + +/*! Converts the given image data to a JPG representation if necessary */ +NSData *_Nullable FBToJpegData(NSData *imageData, CGFloat compressionQuality); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.m b/WebDriverAgentLib/Utilities/FBImageUtils.m new file mode 100644 index 0000000..d991cea --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBImageUtils.m @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBImageUtils.h" + +#import "FBMacros.h" +#import "FBConfiguration.h" + +// https://en.wikipedia.org/wiki/List_of_file_signatures +static uint8_t PNG_MAGIC[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; +static const NSUInteger PNG_MAGIC_LEN = 8; +static uint8_t JPG_MAGIC[] = { 0xff, 0xd8, 0xff }; +static const NSUInteger JPG_MAGIC_LEN = 3; + +BOOL FBIsPngImage(NSData *imageData) +{ + if (nil == imageData || [imageData length] < PNG_MAGIC_LEN) { + return NO; + } + + static NSData* pngMagicStartData = nil; + static dispatch_once_t oncePngToken; + dispatch_once(&oncePngToken, ^{ + pngMagicStartData = [NSData dataWithBytesNoCopy:(void*)PNG_MAGIC length:PNG_MAGIC_LEN freeWhenDone:NO]; + }); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wassign-enum" + NSRange range = [imageData rangeOfData:pngMagicStartData options:kNilOptions range:NSMakeRange(0, PNG_MAGIC_LEN)]; +#pragma clang diagnostic pop + return range.location != NSNotFound; +} + +NSData *FBToPngData(NSData *imageData) { + if (nil == imageData || [imageData length] < PNG_MAGIC_LEN) { + return nil; + } + if (FBIsPngImage(imageData)) { + return imageData; + } + + UIImage *image = [UIImage imageWithData:imageData]; + return nil == image ? nil : (NSData *)UIImagePNGRepresentation(image); +} + +BOOL FBIsJpegImage(NSData *imageData) +{ + if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) { + return NO; + } + + static NSData* jpgMagicStartData = nil; + static dispatch_once_t onceJpgToken; + dispatch_once(&onceJpgToken, ^{ + jpgMagicStartData = [NSData dataWithBytesNoCopy:(void*)JPG_MAGIC length:JPG_MAGIC_LEN freeWhenDone:NO]; + }); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wassign-enum" + NSRange range = [imageData rangeOfData:jpgMagicStartData options:kNilOptions range:NSMakeRange(0, JPG_MAGIC_LEN)]; +#pragma clang diagnostic pop + return range.location != NSNotFound; +} + +NSData *FBToJpegData(NSData *imageData, CGFloat compressionQuality) { + if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) { + return nil; + } + if (FBIsJpegImage(imageData)) { + return imageData; + } + + UIImage *image = [UIImage imageWithData:imageData]; + return nil == image ? nil : (NSData *)UIImageJPEGRepresentation(image, compressionQuality); +} diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.h b/WebDriverAgentLib/Utilities/FBKeyboard.h new file mode 100644 index 0000000..c3c5f8e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBKeyboard.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBKeyboard : NSObject + +#if (!TARGET_OS_TV && __clang_major__ >= 15) +/** + Transforms key name to its string representation, which could be used with XCTest + + @param name one of available keyboard key names defined in https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc + @return Either the key value or nil if no matches have been found + */ ++ (nullable NSString *)keyValueForName:(NSString *)name; +#endif + +/** + Waits until the keyboard is visible on the screen or a timeout happens + + @param app that should be typed + @param timeout the maximum duration in seconds to wait until the keyboard is visible. If the timeout value is equal or less than zero then immediate visibility verification is going to be performed. + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the keyboard is visible after the timeout, otherwise NO. + */ ++ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.m b/WebDriverAgentLib/Utilities/FBKeyboard.m new file mode 100644 index 0000000..af3529e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBKeyboard.m @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBKeyboard.h" + +#import "FBConfiguration.h" +#import "FBXCTestDaemonsProxy.h" +#import "FBErrorBuilder.h" +#import "FBRunLoopSpinner.h" +#import "FBMacros.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCTestDriver.h" +#import "FBLogger.h" +#import "FBConfiguration.h" + +@implementation FBKeyboard + ++ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app + timeout:(NSTimeInterval)timeout + error:(NSError **)error +{ + BOOL (^isKeyboardVisible)(void) = ^BOOL(void) { + XCUIElement *keyboard = app.keyboards.fb_firstMatch; + if (nil == keyboard) { + return NO; + } + + NSPredicate *keySearchPredicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, + NSDictionary *bindings) { + return snapshot.label.length > 0; + }]; + XCUIElement *firstKey = [[keyboard descendantsMatchingType:XCUIElementTypeKey] + matchingPredicate:keySearchPredicate].allElementsBoundByIndex.firstObject; + return firstKey.exists && firstKey.hittable; + }; + NSString* errMessage = @"The on-screen keyboard must be present to send keys"; + if (timeout <= 0) { + if (!isKeyboardVisible()) { + return [[[FBErrorBuilder builder] withDescription:errMessage] buildError:error]; + } + return YES; + } + return + [[[[FBRunLoopSpinner new] + timeout:timeout] + timeoutErrorMessage:errMessage] + spinUntilTrue:isKeyboardVisible + error:error]; +} + +#if (!TARGET_OS_TV && __clang_major__ >= 15) + ++ (NSString *)keyValueForName:(NSString *)name +{ + static dispatch_once_t onceKeys; + static NSDictionary *keysMapping; + dispatch_once(&onceKeys, ^{ + keysMapping = @{ + @"XCUIKeyboardKeyDelete": XCUIKeyboardKeyDelete, + @"XCUIKeyboardKeyReturn": XCUIKeyboardKeyReturn, + @"XCUIKeyboardKeyEnter": XCUIKeyboardKeyEnter, + @"XCUIKeyboardKeyTab": XCUIKeyboardKeyTab, + @"XCUIKeyboardKeySpace": XCUIKeyboardKeySpace, + @"XCUIKeyboardKeyEscape": XCUIKeyboardKeyEscape, + + @"XCUIKeyboardKeyUpArrow": XCUIKeyboardKeyUpArrow, + @"XCUIKeyboardKeyDownArrow": XCUIKeyboardKeyDownArrow, + @"XCUIKeyboardKeyLeftArrow": XCUIKeyboardKeyLeftArrow, + @"XCUIKeyboardKeyRightArrow": XCUIKeyboardKeyRightArrow, + + @"XCUIKeyboardKeyF1": XCUIKeyboardKeyF1, + @"XCUIKeyboardKeyF2": XCUIKeyboardKeyF2, + @"XCUIKeyboardKeyF3": XCUIKeyboardKeyF3, + @"XCUIKeyboardKeyF4": XCUIKeyboardKeyF4, + @"XCUIKeyboardKeyF5": XCUIKeyboardKeyF5, + @"XCUIKeyboardKeyF6": XCUIKeyboardKeyF6, + @"XCUIKeyboardKeyF7": XCUIKeyboardKeyF7, + @"XCUIKeyboardKeyF8": XCUIKeyboardKeyF8, + @"XCUIKeyboardKeyF9": XCUIKeyboardKeyF9, + @"XCUIKeyboardKeyF10": XCUIKeyboardKeyF10, + @"XCUIKeyboardKeyF11": XCUIKeyboardKeyF11, + @"XCUIKeyboardKeyF12": XCUIKeyboardKeyF12, + @"XCUIKeyboardKeyF13": XCUIKeyboardKeyF13, + @"XCUIKeyboardKeyF14": XCUIKeyboardKeyF14, + @"XCUIKeyboardKeyF15": XCUIKeyboardKeyF15, + @"XCUIKeyboardKeyF16": XCUIKeyboardKeyF16, + @"XCUIKeyboardKeyF17": XCUIKeyboardKeyF17, + @"XCUIKeyboardKeyF18": XCUIKeyboardKeyF18, + @"XCUIKeyboardKeyF19": XCUIKeyboardKeyF19, + + @"XCUIKeyboardKeyForwardDelete": XCUIKeyboardKeyForwardDelete, + @"XCUIKeyboardKeyHome": XCUIKeyboardKeyHome, + @"XCUIKeyboardKeyEnd": XCUIKeyboardKeyEnd, + @"XCUIKeyboardKeyPageUp": XCUIKeyboardKeyPageUp, + @"XCUIKeyboardKeyPageDown": XCUIKeyboardKeyPageDown, + @"XCUIKeyboardKeyClear": XCUIKeyboardKeyClear, + @"XCUIKeyboardKeyHelp": XCUIKeyboardKeyHelp, + + @"XCUIKeyboardKeyCapsLock": XCUIKeyboardKeyCapsLock, + @"XCUIKeyboardKeyShift": XCUIKeyboardKeyShift, + @"XCUIKeyboardKeyControl": XCUIKeyboardKeyControl, + @"XCUIKeyboardKeyOption": XCUIKeyboardKeyOption, + @"XCUIKeyboardKeyCommand": XCUIKeyboardKeyCommand, + @"XCUIKeyboardKeyRightShift": XCUIKeyboardKeyRightShift, + @"XCUIKeyboardKeyRightControl": XCUIKeyboardKeyRightControl, + @"XCUIKeyboardKeyRightOption": XCUIKeyboardKeyRightOption, + @"XCUIKeyboardKeyRightCommand": XCUIKeyboardKeyRightCommand, + @"XCUIKeyboardKeySecondaryFn": XCUIKeyboardKeySecondaryFn + }; + }); + return keysMapping[name]; +} + +#endif + +@end diff --git a/WebDriverAgentLib/Utilities/FBLogger.h b/WebDriverAgentLib/Utilities/FBLogger.h new file mode 100644 index 0000000..c92b7ef --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBLogger.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A Global Logger object that understands log levels + */ +@interface FBLogger : NSObject + +/** + Log to stdout. + */ ++ (void)log:(NSString *)message; ++ (void)logFmt:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); + +/** + Log to stdout, only if WDA is Verbose + */ ++ (void)verboseLog:(NSString *)message; ++ (void)verboseLogFmt:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBLogger.m b/WebDriverAgentLib/Utilities/FBLogger.m new file mode 100644 index 0000000..060b8f0 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBLogger.m @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBLogger.h" + +#import "FBConfiguration.h" + +@implementation FBLogger + ++ (void)log:(NSString *)message +{ + NSLog(@"%@", message); +} + ++ (void)logFmt:(NSString *)format, ... +{ + va_list args; + va_start(args, format); + NSLogv(format, args); + va_end(args); +} + ++ (void)verboseLog:(NSString *)message +{ + if (!FBConfiguration.verboseLoggingEnabled) { + return; + } + [self log:message]; +} + ++ (void)verboseLogFmt:(NSString *)format, ... +{ + if (!FBConfiguration.verboseLoggingEnabled) { + return; + } + va_list args; + va_start(args, format); + NSLogv(format, args); + va_end(args); +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBMacros.h b/WebDriverAgentLib/Utilities/FBMacros.h new file mode 100644 index 0000000..ba77c2b --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBMacros.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Typedef to help with storing constant strings for enums. +#if __has_feature(objc_arc) + typedef __unsafe_unretained NSString* FBLiteralString; +#else + typedef NSString* FBLiteralString; +#endif + +/*! Returns 'value' or nil if 'value' is an empty string */ +#define FBTransferEmptyStringToNil(value) ([value isEqual:@""] ? nil : value) + +/*! Returns 'value1' or 'value2' if 'value1' is an empty string */ +#define FBFirstNonEmptyValue(value1, value2) ^{ \ + id value1computed = value1; \ + return (value1computed == nil || [value1computed isEqual:@""] ? value2 : value1computed); \ +}() + +/*! Returns 'value' or NSNull if 'value' is nil */ +#define FBValueOrNull(value) ((value) ?: [NSNull null]) + +/*! + Returns name of class property as a string + previously used [class new] errors out on certain classes because new will be declared unavailable + Instead we are casting into a class to get compiler support with property name. +*/ +#define FBStringify(class, property) ({if(NO){[((class *)nil) property];} @#property;}) + +/*! Creates weak type for given 'arg' */ +#define FBWeakify(arg) typeof(arg) __weak wda_weak_##arg = arg + +/*! Creates strong type for FBWeakify-ed 'arg' */ +#define FBStrongify(arg) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + typeof(arg) arg = wda_weak_##arg \ + _Pragma("clang diagnostic pop") + +/*! Returns YES if current system version satisfies the given codition */ +#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame) +#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending) +#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) +#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) +#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending) + +/*! Converts the given number of milliseconds into seconds */ +#define FBMillisToSeconds(ms) ((ms) / 1000.0) + +/*! Converts boolean value to its string representation */ +#define FBBoolToString(b) ((b) ? @"true" : @"false") + diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.h b/WebDriverAgentLib/Utilities/FBMathUtils.h new file mode 100644 index 0000000..2dbce05 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBMathUtils.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class XCUIApplication; + +extern CGFloat FBDefaultFrameFuzzyThreshold; + +/*! Returns center point of given rect */ +CGPoint FBRectGetCenter(CGRect rect); + +/*! Returns whether floatss are equal within given threshold */ +BOOL FBFloatFuzzyEqualToFloat(CGFloat float1, CGFloat float2, CGFloat threshold); + +/*! Returns whether points are equal within given threshold */ +BOOL FBPointFuzzyEqualToPoint(CGPoint point1, CGPoint point2, CGFloat threshold); + +/*! Returns whether vectors are equal within given threshold */ +BOOL FBVectorFuzzyEqualToVector(CGVector a, CGVector b, CGFloat threshold); + +/*! Returns whether size are equal within given threshold */ +BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold); + +/*! Returns whether rect are equal within given threshold */ +BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold); + +#if !TARGET_OS_TV +/*! Inverts size if necessary to match current screen orientation */ +CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation); +#endif diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.m b/WebDriverAgentLib/Utilities/FBMathUtils.m new file mode 100644 index 0000000..87a7ecf --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBMathUtils.m @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBMathUtils.h" + +#import "FBMacros.h" + +CGFloat FBDefaultFrameFuzzyThreshold = 2.0; + +CGPoint FBRectGetCenter(CGRect rect) +{ + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +BOOL FBFloatFuzzyEqualToFloat(CGFloat float1, CGFloat float2, CGFloat threshold) +{ + return (fabs(float1 - float2) <= threshold); +} + +BOOL FBVectorFuzzyEqualToVector(CGVector a, CGVector b, CGFloat threshold) +{ + return FBFloatFuzzyEqualToFloat(a.dx, b.dx, threshold) && FBFloatFuzzyEqualToFloat(a.dy, b.dy, threshold); +} + +BOOL FBPointFuzzyEqualToPoint(CGPoint point1, CGPoint point2, CGFloat threshold) +{ + return FBFloatFuzzyEqualToFloat(point1.x, point2.x, threshold) && FBFloatFuzzyEqualToFloat(point1.y, point2.y, threshold); +} + +BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold) +{ + return FBFloatFuzzyEqualToFloat(size1.width, size2.width, threshold) && FBFloatFuzzyEqualToFloat(size1.height, size2.height, threshold); +} + +BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold) +{ + return + FBPointFuzzyEqualToPoint(FBRectGetCenter(rect1), FBRectGetCenter(rect2), threshold) && + FBSizeFuzzyEqualToSize(rect1.size, rect2.size, threshold); +} + +#if !TARGET_OS_TV + +CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation) +{ + if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) { + /* + There is an XCTest bug that application.frame property returns exchanged dimensions for landscape mode. + This verification is just to make sure the bug is still there (since height is never greater than width in landscape) + and to make it still working properly after XCTest itself starts to respect landscape mode. + */ + if (actualSize.height > actualSize.width) { + return CGSizeMake(actualSize.height, actualSize.width); + } + } + return actualSize; +} +#endif diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgentLib/Utilities/FBMjpegServer.h new file mode 100644 index 0000000..294c399 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTCPSocket.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FBMjpegServer : NSObject + +/** + The default constructor for the screenshot bradcaster service. + This service sends low resolution screenshots 10 times per seconds + to all connected clients. + */ +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m new file mode 100644 index 0000000..4c786ba --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBMjpegServer.h" + +#import +@import UniformTypeIdentifiers; + +#import "GCDAsyncSocket.h" +#import "FBConfiguration.h" +#import "FBLogger.h" +#import "FBScreenshot.h" +#import "FBImageProcessor.h" +#import "FBImageUtils.h" +#import "XCUIScreen.h" + +static const NSUInteger MAX_FPS = 60; +static const NSTimeInterval FRAME_TIMEOUT = 1.; + +static NSString *const SERVER_NAME = @"WDA MJPEG Server"; +static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue"; + + +@interface FBMjpegServer() + +@property (nonatomic, readonly) dispatch_queue_t backgroundQueue; +@property (nonatomic, readonly) NSMutableArray *listeningClients; +@property (nonatomic, readonly) FBImageProcessor *imageProcessor; +@property (nonatomic, readonly) long long mainScreenID; + +@end + + +@implementation FBMjpegServer + +- (instancetype)init +{ + if ((self = [super init])) { + _listeningClients = [NSMutableArray array]; + dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); + _backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes); + dispatch_async(_backgroundQueue, ^{ + [self streamScreenshot]; + }); + _imageProcessor = [[FBImageProcessor alloc] init]; + _mainScreenID = [XCUIScreen.mainScreen displayID]; + } + return self; +} + +- (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted +{ + uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; + int64_t nextTickDelta = timerInterval - timeElapsed; + if (nextTickDelta > 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ + [self streamScreenshot]; + }); + } else { + // Try to do our best to keep the FPS at a decent level + dispatch_async(self.backgroundQueue, ^{ + [self streamScreenshot]; + }); + } +} + +- (void)streamScreenshot +{ + NSUInteger framerate = FBConfiguration.mjpegServerFramerate; + uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC); + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); + @synchronized (self.listeningClients) { + if (0 == self.listeningClients.count) { + [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; + return; + } + } + + NSError *error; + CGFloat compressionQuality = MAX(FBMinCompressionQuality, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID + compressionQuality:compressionQuality + uti:UTTypeJPEG + timeout:FRAME_TIMEOUT + error:&error]; + if (nil == screenshotData) { + [FBLogger logFmt:@"%@", error.description]; + [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; + return; + } + + CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0; + [self.imageProcessor submitImageData:screenshotData + scalingFactor:scalingFactor + completionHandler:^(NSData * _Nonnull scaled) { + [self sendScreenshot:scaled]; + }]; + + [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; +} + +- (void)sendScreenshot:(NSData *)screenshotData { + NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)]; + NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; + [chunk appendData:screenshotData]; + [chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + @synchronized (self.listeningClients) { + for (GCDAsyncSocket *client in self.listeningClients) { + [client writeData:chunk withTimeout:-1 tag:0]; + } + } +} + +- (void)didClientConnect:(GCDAsyncSocket *)newClient +{ + [FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort]; + // Start broadcast only after there is any data from the client + [newClient readDataWithTimeout:-1 tag:0]; +} + +- (void)didClientSendData:(GCDAsyncSocket *)client +{ + @synchronized (self.listeningClients) { + if ([self.listeningClients containsObject:client]) { + return; + } + } + + [FBLogger logFmt:@"Starting screenshots broadcast for the client at %@:%d", client.connectedHost, client.connectedPort]; + NSString *streamHeader = [NSString stringWithFormat:@"HTTP/1.0 200 OK\r\nServer: %@\r\nConnection: close\r\nMax-Age: 0\r\nExpires: 0\r\nCache-Control: no-cache, private\r\nPragma: no-cache\r\nContent-Type: multipart/x-mixed-replace; boundary=--BoundaryString\r\n\r\n", SERVER_NAME]; + [client writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0]; + @synchronized (self.listeningClients) { + [self.listeningClients addObject:client]; + } +} + +- (void)didClientDisconnect:(GCDAsyncSocket *)client +{ + @synchronized (self.listeningClients) { + [self.listeningClients removeObject:client]; + } + [FBLogger log:@"Disconnected a client from screenshots broadcast"]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBNotificationsHelper.h b/WebDriverAgentLib/Utilities/FBNotificationsHelper.h new file mode 100644 index 0000000..b337cb1 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBNotificationsHelper.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBNotificationsHelper : NSObject + +/** + Creates an expectation that is fulfilled when an expected NSNotification is received + + @param name The name of the awaited notification + @param timeout The maximum amount of float seconds to wait for the expectation + @return The appropriate waiter result + */ ++ (XCTWaiterResult)waitForNotificationWithName:(NSNotificationName)name + timeout:(NSTimeInterval)timeout; + +/** + Creates an expectation that is fulfilled when an expected Darwin notification is received + + @param name The name of the awaited notification + @param timeout The maximum amount of float seconds to wait for the expectation + @return The appropriate waiter result + */ ++ (XCTWaiterResult)waitForDarwinNotificationWithName:(NSString *)name + timeout:(NSTimeInterval)timeout; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBNotificationsHelper.m b/WebDriverAgentLib/Utilities/FBNotificationsHelper.m new file mode 100644 index 0000000..4570478 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBNotificationsHelper.m @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBNotificationsHelper.h" + +@implementation FBNotificationsHelper + ++ (XCTWaiterResult)waitForNotificationWithName:(NSNotificationName)name + timeout:(NSTimeInterval)timeout +{ + XCTNSNotificationExpectation *expectation = [[XCTNSNotificationExpectation alloc] + initWithName:name]; + return [XCTWaiter waitForExpectations:@[expectation] timeout:timeout]; +} + ++ (XCTWaiterResult)waitForDarwinNotificationWithName:(NSString *)name + timeout:(NSTimeInterval)timeout +{ + XCTDarwinNotificationExpectation *expectation = [[XCTDarwinNotificationExpectation alloc] + initWithNotificationName:name]; + return [XCTWaiter waitForExpectations:@[expectation] timeout:timeout]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBPasteboard.h b/WebDriverAgentLib/Utilities/FBPasteboard.h new file mode 100644 index 0000000..f61d7d6 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBPasteboard.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_TV +@interface FBPasteboard : NSObject + +/** + Sets data to the general pasteboard + + @param data base64-encoded string containing the data chunk which is going to be written to the pasteboard + @param type one of the possible data types to set: plaintext, url, image + @param error If there is an error, upon return contains an NSError object that describes the problem + @return YES if the operation was successful + */ ++ (BOOL)setData:(NSData *)data forType:(NSString *)type error:(NSError **)error; + +/** + Gets the data contained in the general pasteboard + + @param type one of the possible data types to get: plaintext, url, image + @param error If there is an error, upon return contains an NSError object that describes the problem + @return NSData object, containing the pasteboard content or an empty string if the pasteboard is empty. + nil is returned if there was an error while getting the data from the pasteboard + */ ++ (nullable NSData *)dataForType:(NSString *)type error:(NSError **)error; + +@end +#endif + +NS_ASSUME_NONNULL_END + diff --git a/WebDriverAgentLib/Utilities/FBPasteboard.m b/WebDriverAgentLib/Utilities/FBPasteboard.m new file mode 100644 index 0000000..08e4e9f --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBPasteboard.m @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBPasteboard.h" + +#import +#import "FBAlert.h" +#import "FBErrorBuilder.h" +#import "FBMacros.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIApplication+FBAlert.h" + +#define ALERT_TIMEOUT_SEC 30 +// Must not be less than FB_MONTORING_INTERVAL in FBAlertsMonitor +#define ALERT_CHECK_INTERVAL_SEC 2 + +#if !TARGET_OS_TV +@implementation FBPasteboard + ++ (BOOL)setData:(NSData *)data forType:(NSString *)type error:(NSError **)error +{ + UIPasteboard *pb = UIPasteboard.generalPasteboard; + if ([type.lowercaseString isEqualToString:@"plaintext"]) { + pb.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } else if ([type.lowercaseString isEqualToString:@"image"]) { + UIImage *image = [UIImage imageWithData:data]; + if (nil == image) { + NSString *description = @"No image can be parsed from the given pasteboard data"; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return NO; + } + pb.image = image; + } else if ([type.lowercaseString isEqualToString:@"url"]) { + NSString *urlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSURL *url = [[NSURL alloc] initWithString:urlString]; + if (nil == url) { + NSString *description = @"No URL can be parsed from the given pasteboard data"; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return NO; + } + pb.URL = url; + } else { + NSString *description = [NSString stringWithFormat:@"Unsupported content type: %@", type]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return NO; + } + return YES; +} + ++ (nullable id)pasteboardContentForItem:(NSString *)item + instance:(UIPasteboard *)pbInstance + timeout:(NSTimeInterval)timeout + error:(NSError **)error +{ + SEL selector = NSSelectorFromString(item); + NSMethodSignature *methodSignature = [pbInstance methodSignatureForSelector:selector]; + if (nil == methodSignature) { + NSString *description = [NSString stringWithFormat:@"Cannot retrieve '%@' from a UIPasteboard instance", item]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setTarget:pbInstance]; + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + [invocation invoke]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + return result; + } + + // https://github.com/appium/appium/issues/17392 + __block id pasteboardContent; + dispatch_queue_t backgroundQueue = dispatch_queue_create("GetPasteboard", NULL); + __block BOOL didFinishGetPasteboard = NO; + dispatch_async(backgroundQueue, ^{ + [invocation invoke]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + pasteboardContent = result; + didFinishGetPasteboard = YES; + }); + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); + while (!didFinishGetPasteboard) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:ALERT_CHECK_INTERVAL_SEC]]; + if (didFinishGetPasteboard) { + break; + } + + XCUIElement *alertElement = XCUIApplication.fb_systemApplication.fb_alertElement; + if (nil != alertElement) { + FBAlert *alert = [FBAlert alertWithElement:alertElement]; + [alert acceptWithError:nil]; + } + uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; + if (timeElapsed / NSEC_PER_SEC > timeout) { + NSString *description = [NSString stringWithFormat:@"Cannot handle pasteboard alert within %@s timeout", @(timeout)]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + } + return pasteboardContent; +} + ++ (NSData *)dataForType:(NSString *)type error:(NSError **)error +{ + UIPasteboard *pb = UIPasteboard.generalPasteboard; + if ([type.lowercaseString isEqualToString:@"plaintext"]) { + if (pb.hasStrings) { + id result = [self.class pasteboardContentForItem:@"strings" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + return nil == result + ? nil + : [[(NSArray *)result componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; + } + } else if ([type.lowercaseString isEqualToString:@"image"]) { + if (pb.hasImages) { + id result = [self.class pasteboardContentForItem:@"image" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + return nil == result ? nil : UIImagePNGRepresentation((UIImage *)result); + } + } else if ([type.lowercaseString isEqualToString:@"url"]) { + if (pb.hasURLs) { + id result = [self.class pasteboardContentForItem:@"URLs" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + if (nil == result) { + return nil; + } + NSMutableArray *urls = [NSMutableArray array]; + for (NSURL *url in (NSArray *)result) { + if (nil != url.absoluteString) { + [urls addObject:(id)url.absoluteString]; + } + } + return [[urls componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; + } + } else { + NSString *description = [NSString stringWithFormat:@"Unsupported content type: %@", type]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + return [@"" dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end +#endif diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.h b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h new file mode 100644 index 0000000..e25573e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Prepares an element dictionary, which could be then used in hybrid W3C/JWP responses + + @param element Either element identifier or element object itself + @returns The resulting dictionary + */ +NSDictionary *FBToElementDict(id element); + +/** + Extracts element uuid from dictionary + + @param src The source dictionary + @returns The resulting element or nil if no element keys are found + */ +id _Nullable FBExtractElement(NSDictionary *src); + +/** + Cleanup items having element keys from the dictionary + + @param src The source dictionary + @returns The resulting dictionary + */ +NSDictionary *FBCleanupElements(NSDictionary *src); + +/** + Parses key/value pairs of valid W3C capabilities + + @param caps The source capabilitites dictionary + @param error Is set if there was an error while parsing the source capabilities + @returns Parsed capabilitites mapping or nil in case of failure + */ +NSDictionary *_Nullable FBParseCapabilities(NSDictionary *caps, NSError **error); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.m b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m new file mode 100644 index 0000000..a463294 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBProtocolHelpers.h" + +#import "FBErrorBuilder.h" +#import "FBLogger.h" + +static NSString *const W3C_ELEMENT_KEY = @"element-6066-11e4-a52e-4f735466cecf"; +static NSString *const JSONWP_ELEMENT_KEY = @"ELEMENT"; + +static NSString *const APPIUM_PREFIX = @"appium"; +static NSString *const ALWAYS_MATCH_KEY = @"alwaysMatch"; +static NSString *const FIRST_MATCH_KEY = @"firstMatch"; + + +NSDictionary *FBToElementDict(id element) +{ + return @{ + W3C_ELEMENT_KEY: element, + JSONWP_ELEMENT_KEY: element + }; +} + +id FBExtractElement(NSDictionary *src) +{ + for (NSString* key in src) { + if ([key.lowercaseString isEqualToString:W3C_ELEMENT_KEY.lowercaseString] + || [key.lowercaseString isEqualToString:JSONWP_ELEMENT_KEY.lowercaseString]) { + return src[key]; + } + } + return nil; +} + +NSDictionary *FBCleanupElements(NSDictionary *src) +{ + NSMutableDictionary *result = src.mutableCopy; + for (NSString* key in src) { + if ([key.lowercaseString isEqualToString:W3C_ELEMENT_KEY.lowercaseString] + || [key.lowercaseString isEqualToString:JSONWP_ELEMENT_KEY.lowercaseString]) { + [result removeObjectForKey:key]; + } + } + return result.copy; +} + +NSArray *standardCapabilities(void) +{ + static NSArray *standardCaps; + static dispatch_once_t onceStandardCaps; + dispatch_once(&onceStandardCaps, ^{ + standardCaps = @[ + @"browserName", + @"browserVersion", + @"platformName", + @"acceptInsecureCerts", + @"pageLoadStrategy", + @"proxy", + @"setWindowRect", + @"timeouts", + @"unhandledPromptBehavior" + ]; + }); + return standardCaps; +} + +BOOL isStandardCap(NSString *capName) +{ + return [standardCapabilities() containsObject:capName]; +} + +NSDictionary *_Nullable mergeCaps(NSDictionary *primary, NSDictionary *secondary, NSError **error) +{ + NSMutableDictionary *result = primary.mutableCopy; + for (NSString *capName in secondary) { + if (nil != result[capName]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Property '%@' should not exist on both primary (%@) and secondary (%@) objects", capName, primary, secondary] + buildError:error]; + return nil; + } + [result setObject:(id) secondary[capName] forKey:capName]; + } + return result.copy; +} + +NSDictionary *_Nullable stripPrefixes(NSDictionary *caps, NSError **error) +{ + NSString* prefix = [NSString stringWithFormat:@"%@:", APPIUM_PREFIX]; + NSMutableDictionary *filteredCaps = [NSMutableDictionary dictionary]; + NSMutableArray *badPrefixedCaps = [NSMutableArray array]; + for (NSString *capName in caps) { + if (![capName hasPrefix:prefix]) { + [filteredCaps setObject:(id) caps[capName] forKey:capName]; + continue; + } + + NSString *strippedName = [capName substringFromIndex:prefix.length]; + [filteredCaps setObject:(id) caps[capName] forKey:strippedName]; + if (isStandardCap(strippedName)) { + [badPrefixedCaps addObject:strippedName]; + } + } + if (badPrefixedCaps.count > 0) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The capabilities %@ are standard and should not have the '%@' prefix", badPrefixedCaps, prefix] + buildError:error]; + return nil; + } + return filteredCaps.copy; +} + +NSDictionary *FBParseCapabilities(NSDictionary *caps, NSError **error) +{ + NSDictionary *alwaysMatch = caps[ALWAYS_MATCH_KEY] ?: @{}; + NSArray *> *firstMatch = caps[FIRST_MATCH_KEY] ?: @[]; + NSArray *> *allFirstMatchCaps = firstMatch.count == 0 ? @[@{}] : firstMatch; + NSDictionary *requiredCaps; + if (nil == (requiredCaps = stripPrefixes(alwaysMatch, error))) { + return nil; + } + for (NSDictionary *fmc in allFirstMatchCaps) { + NSDictionary *strippedCaps; + if (nil == (strippedCaps = stripPrefixes(fmc, error))) { + return nil; + } + NSDictionary *mergedCaps; + if (nil == (mergedCaps = mergeCaps(requiredCaps, strippedCaps, error))) { + [FBLogger logFmt:@"%@", (*error).description]; + continue; + } + return mergedCaps; + } + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Could not find matching capabilities from %@", caps] + buildError:error]; + return nil; +} diff --git a/WebDriverAgentLib/Utilities/FBReflectionUtils.h b/WebDriverAgentLib/Utilities/FBReflectionUtils.h new file mode 100644 index 0000000..262702a --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBReflectionUtils.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Swizzles the implemntation of originalSelector with the swizzledSelector for the given class. + * Both methods must belong to this class. + * + * @param cls The class where to swizzle + * @param originalSelector original method selector + * @paramswizzledSelector swizzled method selector + */ +void FBReplaceMethod(Class cls, SEL originalSelector, SEL swizzledSelector); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBReflectionUtils.m b/WebDriverAgentLib/Utilities/FBReflectionUtils.m new file mode 100644 index 0000000..8ef8419 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBReflectionUtils.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBReflectionUtils.h" + +#import + +void FBReplaceMethod(Class class, SEL originalSelector, SEL swizzledSelector) { + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); + + BOOL didAddMethod = + class_addMethod(class, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)); + + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzledMethod); + } +} diff --git a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h new file mode 100644 index 0000000..5f8faf5 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef BOOL (^FBRunLoopSpinnerBlock)(void); +typedef __nullable id (^FBRunLoopSpinnerObjectBlock)(void); + +@interface FBRunLoopSpinner : NSObject + +/** + Dispatches block and spins the run loop until `completion` block is called. + + @param block the block to wait for to finish. + */ ++ (void)spinUntilCompletion:(void (^)(void(^completion)(void)))block; + +/** + Updates the error message to print in the event of a timeout. + + @param timeoutErrorMessage the Error Message to print. + @return the receiver, for chaining. + */ +- (instancetype)timeoutErrorMessage:(NSString *)timeoutErrorMessage; + +/** + Updates the timeout of the receiver. + + @param timeout the amount of time to wait before timing out. + @return the receiver, for chaining. + */ +- (instancetype)timeout:(NSTimeInterval)timeout; + +/** + Updates the interval of the receiver. + + @param interval the amount of time to wait before checking condition again. + @return the receiver, for chaining. + */ +- (instancetype)interval:(NSTimeInterval)interval; + +/** + Spins the Run Loop until `untilTrue` returns YES or a timeout is reached. + + @param untilTrue the condition to meet. + @return YES if the condition was met, NO if the timeout was reached first. + */ +- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue; + +/** + Spins the Run Loop until `untilTrue` returns YES or a timeout is reached. + + @param untilTrue the condition to meet. + @param error to fill in case of timeout. + @return YES if the condition was met, NO if the timeout was reached first. + */ +- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue error:(NSError **)error; + +/** + Spins the Run Loop until `untilNotNil` returns non nil value or a timeout is reached. + + @param untilNotNil the condition to meet. + @param error to fill in case of timeout. + @return YES if the condition was met, NO if the timeout was reached first. + */ +- (nullable id)spinUntilNotNil:(FBRunLoopSpinnerObjectBlock)untilNotNil error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m new file mode 100644 index 0000000..0912325 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBRunLoopSpinner.h" + +#import + +#import "FBErrorBuilder.h" + +static const NSTimeInterval FBWaitInterval = 0.1; + +@interface FBRunLoopSpinner () +@property (nonatomic, copy) NSString *timeoutErrorMessage; +@property (nonatomic, assign) NSTimeInterval timeout; +@property (nonatomic, assign) NSTimeInterval interval; +@end + +@implementation FBRunLoopSpinner + ++ (void)spinUntilCompletion:(void (^)(void(^completion)(void)))block +{ + __block volatile atomic_bool didFinish = false; + block(^{ + atomic_fetch_or(&didFinish, true); + }); + while (!atomic_fetch_and(&didFinish, false)) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBWaitInterval]]; + } +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _interval = FBWaitInterval; + _timeout = 60; + } + return self; +} + +- (instancetype)timeoutErrorMessage:(NSString *)timeoutErrorMessage +{ + self.timeoutErrorMessage = timeoutErrorMessage; + return self; +} + +- (instancetype)timeout:(NSTimeInterval)timeout +{ + self.timeout = timeout; + return self; +} + +- (instancetype)interval:(NSTimeInterval)interval +{ + self.interval = interval; + return self; +} + +- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue +{ + return [self spinUntilTrue:untilTrue error:nil]; +} + +- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue error:(NSError **)error +{ + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:self.timeout]; + while (!untilTrue()) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:self.interval]]; + if (timeoutDate.timeIntervalSinceNow < 0) { + return + [[[FBErrorBuilder builder] + withDescription:(self.timeoutErrorMessage ?: @"FBRunLoopSpinner timeout")] + buildError:error]; + } + } + return YES; +} + +- (id)spinUntilNotNil:(FBRunLoopSpinnerObjectBlock)untilNotNil error:(NSError **)error +{ + __block id object; + [self spinUntilTrue:^BOOL{ + object = untilNotNil(); + return object != nil; + } error:error]; + return object; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBRuntimeUtils.h b/WebDriverAgentLib/Utilities/FBRuntimeUtils.h new file mode 100644 index 0000000..b16b123 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBRuntimeUtils.h @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Returns array of classes that conforms to given protocol + */ +NSArray *FBClassesThatConformsToProtocol(Protocol *protocol); + +/** + Method used to retrieve pointer for given symbol 'name' from given 'binary' + + @param binary path to binary we want to retrieve symbols pointer from + @param name name of the symbol + @return pointer to symbol + */ +void *FBRetrieveSymbolFromBinary(const char *binary, const char *name); + +/** + Get the compiler SDK version as string. + + @return SDK version as string, for example "10.0" or nil if it cannot be received + */ +NSString * _Nullable FBSDKVersion(void); + +/** + Check if the compiler SDK version is less than the given version. + The current iOS version is taken instead if SDK version cannot be retrieved. + + @param expected the expected version to compare with, for example '10.3' + @return YES if the given version is less than the SDK version used for WDA compilation + */ +BOOL isSDKVersionLessThan(NSString *expected); + +/** + Check if the compiler SDK version is less or equal to the given version. + The current iOS version is taken instead if SDK version cannot be retrieved. + + @param expected the expected version to compare with, for example '10.3' + @return YES if the given version is less or equal to the SDK version used for WDA compilation + */ +BOOL isSDKVersionLessThanOrEqualTo(NSString *expected); + +/** + Check if the compiler SDK version is equal to the given version. + The current iOS version is taken instead if SDK version cannot be retrieved. + + @param expected the expected version to compare with, for example '10.3' + @return YES if the given version is equal to the SDK version used for WDA compilation + */ +BOOL isSDKVersionEqualTo(NSString *expected); + +/** + Check if the compiler SDK version is greater or equal to the given version. + The current iOS version is taken instead if SDK version cannot be retrieved. + + @param expected the expected version to compare with, for example '10.3' + @return YES if the given version is greater or equal to the SDK version used for WDA compilation + */ +BOOL isSDKVersionGreaterThanOrEqualTo(NSString *expected); + +/** + Check if the compiler SDK version is greater than the given version. + The current iOS version is taken instead if SDK version cannot be retrieved. + + @param expected the expected version to compare with, for example '10.3' + @return YES if the given version is greater than the SDK version used for WDA compilation + */ +BOOL isSDKVersionGreaterThan(NSString *expected); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBRuntimeUtils.m b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m new file mode 100644 index 0000000..6904cf7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBRuntimeUtils.h" + +#import "FBMacros.h" +#import "XCUIDevice.h" + +#include +#import + +NSArray *FBClassesThatConformsToProtocol(Protocol *protocol) +{ + Class *classes = NULL; + NSMutableArray *collection = [NSMutableArray array]; + int numClasses = objc_getClassList(NULL, 0); + if (numClasses == 0 ) { + return @[]; + } + + classes = (__unsafe_unretained Class*)malloc(sizeof(Class) * numClasses); + numClasses = objc_getClassList(classes, numClasses); + for (int index = 0; index < numClasses; index++) { + Class aClass = classes[index]; + if (class_conformsToProtocol(aClass, protocol)) { + [collection addObject:aClass]; + } + } + free(classes); + return collection.copy; +} + +void *FBRetrieveSymbolFromBinary(const char *binary, const char *name) +{ + void *handle = dlopen(binary, RTLD_LAZY); + NSCAssert(handle, @"%s could not be opened", binary); + void *pointer = dlsym(handle, name); + NSCAssert(pointer, @"%s could not be located", name); + return pointer; +} + +static NSString *sdkVersion = nil; +static dispatch_once_t onceSdkVersionToken; +NSString * _Nullable FBSDKVersion(void) +{ + dispatch_once(&onceSdkVersionToken, ^{ + NSString *sdkName = [[NSBundle mainBundle] infoDictionary][@"DTSDKName"]; + if (sdkName) { + // the value of DTSDKName looks like 'iphoneos9.2' + NSRange versionRange = [sdkName rangeOfString:@"\\d+\\.\\d+" options:NSRegularExpressionSearch]; + if (versionRange.location != NSNotFound) { + sdkVersion = [sdkName substringWithRange:versionRange]; + } + } + }); + return sdkVersion; +} + +BOOL isSDKVersionLessThan(NSString *expected) +{ + NSString *version = FBSDKVersion(); + if (nil == version) { + return SYSTEM_VERSION_LESS_THAN(expected); + } + NSComparisonResult result = [version compare:expected options:NSNumericSearch]; + return result == NSOrderedAscending; +} + +BOOL isSDKVersionLessThanOrEqualTo(NSString *expected) +{ + NSString *version = FBSDKVersion(); + if (nil == version) { + return SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(expected); + } + NSComparisonResult result = [version compare:expected options:NSNumericSearch]; + return result == NSOrderedAscending || result == NSOrderedSame; +} + +BOOL isSDKVersionEqualTo(NSString *expected) +{ + NSString *version = FBSDKVersion(); + if (nil == version) { + return SYSTEM_VERSION_EQUAL_TO(expected); + } + NSComparisonResult result = [version compare:expected options:NSNumericSearch]; + return result == NSOrderedSame; +} + +BOOL isSDKVersionGreaterThanOrEqualTo(NSString *expected) +{ + NSString *version = FBSDKVersion(); + if (nil == version) { + return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(expected); + } + NSComparisonResult result = [version compare:expected options:NSNumericSearch]; + return result == NSOrderedDescending || result == NSOrderedSame; +} + +BOOL isSDKVersionGreaterThan(NSString *expected) +{ + NSString *version = FBSDKVersion(); + if (nil == version) { + return SYSTEM_VERSION_GREATER_THAN(expected); + } + NSComparisonResult result = [version compare:expected options:NSNumericSearch]; + return result == NSOrderedDescending; +} diff --git a/WebDriverAgentLib/Utilities/FBScreen.h b/WebDriverAgentLib/Utilities/FBScreen.h new file mode 100644 index 0000000..87c22c7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBScreen.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreen : NSObject + +/** + The scale factor of the main device's screen + */ ++ (double)scale; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBScreen.m b/WebDriverAgentLib/Utilities/FBScreen.m new file mode 100644 index 0000000..dc65364 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBScreen.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreen.h" +#import "XCUIElement+FBIsVisible.h" +#import "FBXCodeCompatibility.h" +#import "XCUIScreen.h" + +@implementation FBScreen + ++ (double)scale +{ + return [XCUIScreen.mainScreen scale]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.h b/WebDriverAgentLib/Utilities/FBScreenshot.h new file mode 100644 index 0000000..b0fdfbe --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBScreenshot.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +@class UTType; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenshot : NSObject + +/** + Retrieves non-scaled screenshot of the whole screen + + @param quality The number in range 0-3, where 0 is PNG (lossless), 3 is HEIC (lossless), 1- low quality JPEG and 2 - high quality JPEG + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return Device screenshot as PNG-encoded data or nil in case of failure + */ ++ (nullable NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality + error:(NSError **)error; + +/** + Retrieves non-scaled screenshot of the whole screen + + @param screenID The screen identifier to take the screenshot from + @param compressionQuality Normalized screenshot quality value in range 0..1, where 1 is the best quality + @param uti UTType... constant, which defines the type of the returned screenshot image + @param timeout how much time to allow for the screenshot to be taken + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return Device screenshot as PNG-, HEIC- or JPG-encoded data or nil in case of failure + */ ++ (nullable NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID + compressionQuality:(CGFloat)compressionQuality + uti:(UTType *)uti + timeout:(NSTimeInterval)timeout + error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.m b/WebDriverAgentLib/Utilities/FBScreenshot.m new file mode 100644 index 0000000..f13f967 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBScreenshot.m @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScreenshot.h" + +@import UniformTypeIdentifiers; + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBImageProcessor.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCTestManager_ManagerInterface-Protocol.h" +#import "XCUIScreen.h" + +static const NSTimeInterval SCREENSHOT_TIMEOUT = 20.; +static const CGFloat SCREENSHOT_SCALE = 1.0; // Screenshot API should keep the original screen scale +static const CGFloat HIGH_QUALITY = 0.8; +static const CGFloat LOW_QUALITY = 0.25; + +NSString *formatTimeInterval(NSTimeInterval interval) { + NSUInteger milliseconds = (NSUInteger)(interval * 1000); + return [NSString stringWithFormat:@"%lu ms", milliseconds]; +} + +@implementation FBScreenshot + ++ (CGFloat)compressionQualityWithQuality:(NSUInteger)quality +{ + switch (quality) { + case 1: + return HIGH_QUALITY; + case 2: + return LOW_QUALITY; + default: + return 1.0; + } +} + ++ (UTType *)imageUtiWithQuality:(NSUInteger)quality +{ + switch (quality) { + case 1: + case 2: + return UTTypeJPEG; + case 3: + return UTTypeHEIC; + default: + return UTTypePNG; + } +} + ++ (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality + error:(NSError **)error +{ + XCUIScreen *mainScreen = XCUIScreen.mainScreen; + return [self.class takeWithScreenID:mainScreen.displayID + scale:SCREENSHOT_SCALE + compressionQuality:[self.class compressionQualityWithQuality:quality] + sourceUTI:[self.class imageUtiWithQuality:quality] + error:error]; +} + ++ (NSData *)takeWithScreenID:(long long)screenID + scale:(CGFloat)scale + compressionQuality:(CGFloat)compressionQuality + sourceUTI:(UTType *)uti + error:(NSError **)error +{ + NSData *screenshotData = [self.class takeInOriginalResolutionWithScreenID:screenID + compressionQuality:compressionQuality + uti:uti + timeout:SCREENSHOT_TIMEOUT + error:error]; + if (nil == screenshotData) { + return nil; + } + return [[[FBImageProcessor alloc] init] scaledImageWithData:screenshotData + uti:UTTypePNG + scalingFactor:1.0 / scale + compressionQuality:FBMaxCompressionQuality + error:error]; +} + ++ (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID + compressionQuality:(CGFloat)compressionQuality + uti:(UTType *)uti + timeout:(NSTimeInterval)timeout + error:(NSError **)error +{ + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; + __block NSData *screenshotData = nil; + __block NSError *innerError = nil; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + id screnshotRequest = [self.class screenshotRequestWithScreenID:screenID + rect:CGRectNull + uti:uti + compressionQuality:compressionQuality + error:error]; + if (nil == screnshotRequest) { + return nil; + } + [proxy _XCT_requestScreenshot:screnshotRequest + withReply:^(id image, NSError *err) { + if (nil != err) { + innerError = err; + } else { + screenshotData = [image data]; + } + dispatch_semaphore_signal(sem); + }]; + int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { + NSString *timeoutMsg = [NSString stringWithFormat:@"Cannot take a screenshot within %@ timeout", formatTimeInterval(SCREENSHOT_TIMEOUT)]; + if (nil == error) { + [FBLogger log:timeoutMsg]; + } else if (nil == innerError) { + [[[FBErrorBuilder builder] + withDescription:timeoutMsg] + buildError:error]; + } + }; + if (nil != error && nil != innerError) { + *error = innerError; + } + return screenshotData; +} + ++ (nullable id)imageEncodingWithUniformTypeIdentifier:(UTType *)uti + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error +{ + Class imageEncodingClass = NSClassFromString(@"XCTImageEncoding"); + if (nil == imageEncodingClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTImageEncoding class"] + buildError:error]; + return nil; + } + + if ([uti conformsToType:UTTypeHEIC]) { + static BOOL isHeicSuppported = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL selector = NSSelectorFromString(@"supportsHEICImageEncoding"); + NSMethodSignature *signature = [imageEncodingClass methodSignatureForSelector:selector]; + if (nil != signature) { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:imageEncodingClass]; + [invocation getReturnValue:&isHeicSuppported]; + } + }); + if (!isHeicSuppported) { + [FBLogger logFmt:@"The device under test does not support HEIC image encoding. Falling back to PNG"]; + uti = UTTypePNG; + } + } + + id imageEncodingAllocated = [imageEncodingClass alloc]; + SEL imageEncodingConstructorSelector = NSSelectorFromString(@"initWithUniformTypeIdentifier:compressionQuality:"); + if (![imageEncodingAllocated respondsToSelector:imageEncodingConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithUniformTypeIdentifier:compressionQuality:' contructor is not found on XCTImageEncoding class"] + buildError:error]; + return nil; + } + NSMethodSignature *imageEncodingContructorSignature = [imageEncodingAllocated methodSignatureForSelector:imageEncodingConstructorSelector]; + NSInvocation *imageEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:imageEncodingContructorSignature]; + [imageEncodingInitInvocation setSelector:imageEncodingConstructorSelector]; + NSString *utiIdentifier = uti.identifier; + [imageEncodingInitInvocation setArgument:&utiIdentifier atIndex:2]; + [imageEncodingInitInvocation setArgument:&compressionQuality atIndex:3]; + [imageEncodingInitInvocation invokeWithTarget:imageEncodingAllocated]; + id __unsafe_unretained imageEncoding; + [imageEncodingInitInvocation getReturnValue:&imageEncoding]; + return imageEncoding; +} + ++ (nullable id)screenshotRequestWithScreenID:(long long)screenID + rect:(struct CGRect)rect + uti:(UTType *)uti + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error +{ + id imageEncoding = [self.class imageEncodingWithUniformTypeIdentifier:uti + compressionQuality:compressionQuality + error:error]; + if (nil == imageEncoding) { + return nil; + } + + Class screenshotRequestClass = NSClassFromString(@"XCTScreenshotRequest"); + if (nil == screenshotRequestClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTScreenshotRequest class"] + buildError:error]; + return nil; + } + id screenshotRequestAllocated = [screenshotRequestClass alloc]; + SEL screenshotRequestConstructorSelector = NSSelectorFromString(@"initWithScreenID:rect:encoding:"); + if (![screenshotRequestAllocated respondsToSelector:screenshotRequestConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithScreenID:rect:encoding:' contructor is not found on XCTScreenshotRequest class"] + buildError:error]; + return nil; + } + NSMethodSignature *screenshotRequestContructorSignature = [screenshotRequestAllocated methodSignatureForSelector:screenshotRequestConstructorSelector]; + NSInvocation *screenshotRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenshotRequestContructorSignature]; + [screenshotRequestInitInvocation setSelector:screenshotRequestConstructorSelector]; + [screenshotRequestInitInvocation setArgument:&screenID atIndex:2]; + [screenshotRequestInitInvocation setArgument:&rect atIndex:3]; + [screenshotRequestInitInvocation setArgument:&imageEncoding atIndex:4]; + [screenshotRequestInitInvocation invokeWithTarget:screenshotRequestAllocated]; + id __unsafe_unretained screenshotRequest; + [screenshotRequestInitInvocation getReturnValue:&screenshotRequest]; + return screenshotRequest; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h new file mode 100644 index 0000000..b98a070 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +// See FBConfiguration.h for more details on the meaning of each setting + +extern NSString* const FB_SETTING_USE_COMPACT_RESPONSES; +extern NSString* const FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES; +extern NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY; +extern NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE; +extern NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION; +extern NSString* const FB_SETTING_MJPEG_SCALING_FACTOR; +extern NSString* const FB_SETTING_SCREENSHOT_QUALITY; +extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION; +extern NSString* const FB_SETTING_KEYBOARD_PREDICTION; +extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH; +extern NSString* const FB_SETTING_USE_FIRST_MATCH; +extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX; +extern NSString* const FB_SETTING_REDUCE_MOTION; +extern NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION; +extern NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT; +extern NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS; +extern NSString* const FB_SETTING_DEFAULT_ALERT_ACTION; +extern NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR; +extern NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR; +extern NSString* const FB_SETTING_SCREENSHOT_ORIENTATION; +extern NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT; +extern NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT; +extern NSString* const FB_SETTING_MAX_TYPING_FREQUENCY; +extern NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS; +extern NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT; +extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE; +extern NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR; +extern NSString *const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE; + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m new file mode 100644 index 0000000..17b8ab7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBSettings.h" + +NSString* const FB_SETTING_USE_COMPACT_RESPONSES = @"shouldUseCompactResponses"; +NSString* const FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES = @"elementResponseAttributes"; +NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY = @"mjpegServerScreenshotQuality"; +NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE = @"mjpegServerFramerate"; +NSString* const FB_SETTING_MJPEG_SCALING_FACTOR = @"mjpegScalingFactor"; +NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION = @"mjpegFixOrientation"; +NSString* const FB_SETTING_SCREENSHOT_QUALITY = @"screenshotQuality"; +NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection"; +NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction"; +NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth"; +NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch"; +NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex"; +NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion"; +NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION = @"defaultActiveApplication"; +NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT = @"activeAppDetectionPoint"; +NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS = @"includeNonModalElements"; +NSString* const FB_SETTING_DEFAULT_ALERT_ACTION = @"defaultAlertAction"; +NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR = @"acceptAlertButtonSelector"; +NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR = @"dismissAlertButtonSelector"; +NSString* const FB_SETTING_SCREENSHOT_ORIENTATION = @"screenshotOrientation"; +NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT = @"waitForIdleTimeout"; +NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT = @"animationCoolOffTimeout"; +NSString* const FB_SETTING_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; +NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS = @"respectSystemAlerts"; +NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT = @"useClearTextShortcut"; +NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE = @"limitXPathContextScope"; +NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR = @"autoClickAlertSelector"; +NSString* const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE = @"includeHittableInPageSource"; +NSString* const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE = @"includeNativeFrameInPageSource"; +NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource"; diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h b/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h new file mode 100644 index 0000000..8b7f775 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#if TARGET_OS_TV + +@interface FBTVNavigationItem () +@property (nonatomic, readonly) NSString *uid; +@property (nonatomic, readonly) NSMutableSet* directions; + ++ (instancetype)itemWithUid:(NSString *) uid; +@end + + +@interface FBTVNavigationTracker () + +- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta; +- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta; +@end + +#endif diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h new file mode 100644 index 0000000..3a80583 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#if TARGET_OS_TV + +/** + Defines directions to move focuse to. + */ +typedef NS_ENUM(NSUInteger, FBTVDirection) { + FBTVDirectionUp = 0, + FBTVDirectionDown = 1, + FBTVDirectionLeft = 2, + FBTVDirectionRight = 3, + FBTVDirectionNone = 4 +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBTVNavigationItem : NSObject +@end + +@interface FBTVNavigationTracker : NSObject + +/** + Track the target element's point + + @param targetElement A target element which will track + @return An instancce of FBTVNavigationTracker + */ ++ (instancetype)trackerWithTargetElement: (XCUIElement *) targetElement; + +/** + Determine the correct direction to move the focus to the tracked target + element from the currently focused one + + @return FBTVDirection to move the focus to + */ +- (FBTVDirection)directionToFocusedElement; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m new file mode 100644 index 0000000..19fa4a0 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTVNavigationTracker.h" +#import "FBTVNavigationTracker-Private.h" + +#import "FBMathUtils.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIApplication+FBHelpers.h" + +#if TARGET_OS_TV + +@implementation FBTVNavigationItem + ++ (instancetype)itemWithUid:(NSString *) uid +{ + return [[FBTVNavigationItem alloc] initWithUid:uid]; +} + +- (instancetype)initWithUid:(NSString *) uid +{ + self = [super init]; + if(self) { + _uid = uid; + _directions = [NSMutableSet set]; + } + return self; +} + +@end + +@interface FBTVNavigationTracker () +@property (nonatomic, strong) XCUIElement *targetElement; +@property (nonatomic, assign) CGPoint targetCenter; +@property (nonatomic, strong) NSMutableDictionary* navigationItems; +@end + +@implementation FBTVNavigationTracker + ++ (instancetype)trackerWithTargetElement:(XCUIElement *)targetElement +{ + FBTVNavigationTracker *tracker = [[FBTVNavigationTracker alloc] initWithTargetElement:targetElement]; + tracker.targetElement = targetElement; + return tracker; +} + +- (instancetype)initWithTargetElement:(XCUIElement *)targetElement +{ + self = [super init]; + if (self) { + _targetElement = targetElement; + CGRect frame = targetElement.wdFrame; + _targetCenter = FBRectGetCenter(frame); + _navigationItems = [NSMutableDictionary dictionary]; + } + return self; +} + +- (FBTVDirection)directionToFocusedElement +{ + XCUIElement *focused = XCUIApplication.fb_activeApplication.fb_focusedElement; + + CGPoint focusedCenter = FBRectGetCenter(focused.wdFrame); + FBTVNavigationItem *item = [self navigationItemWithElement:focused]; + CGFloat yDelta = self.targetCenter.y - focusedCenter.y; + CGFloat xDelta = self.targetCenter.x - focusedCenter.x; + FBTVDirection direction; + if (fabs(yDelta) > fabs(xDelta)) { + direction = [self verticalDirectionWithItem:item andDelta:yDelta]; + if (direction == FBTVDirectionNone) { + direction = [self horizontalDirectionWithItem:item andDelta:xDelta]; + } + } else { + direction = [self horizontalDirectionWithItem:item andDelta:xDelta]; + if (direction == FBTVDirectionNone) { + direction = [self verticalDirectionWithItem:item andDelta:yDelta]; + } + } + + return direction; +} + +#pragma mark - Utilities +- (FBTVNavigationItem*)navigationItemWithElement:(id)element +{ + NSString *uid = element.wdUID; + if (nil == uid) { + return nil; + } + + FBTVNavigationItem* item = [self.navigationItems objectForKey:uid]; + if (nil != item) { + return item; + } + + item = [FBTVNavigationItem itemWithUid:uid]; + [self.navigationItems setObject:item forKey:uid]; + return item; +} + +- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta +{ + // GCFloat is double in 64bit. tvOS is only for arm64 + if (delta > DBL_EPSILON && + ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionRight]]) { + [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionRight]]; + return FBTVDirectionRight; + } + if (delta < -DBL_EPSILON && + ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionLeft]]) { + [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionLeft]]; + return FBTVDirectionLeft; + } + return FBTVDirectionNone; +} + +- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta +{ + // GCFloat is double in 64bit. tvOS is only for arm64 + if (delta > DBL_EPSILON && + ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionDown]]) { + [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionDown]]; + return FBTVDirectionDown; + } + if (delta < -DBL_EPSILON && + ![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionUp]]) { + [item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionUp]]; + return FBTVDirectionUp; + } + return FBTVDirectionNone; +} + +@end + +#endif diff --git a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h new file mode 100644 index 0000000..f640405 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Launches apps without attaching them to an XCUITest or a WDA session, allowing them to remain open + when WDA closes. +*/ +@interface FBUnattachedAppLauncher : NSObject + +/** + Launch the app with the specified bundle ID. Return YES if successful, NO otherwise. + */ ++ (BOOL)launchAppWithBundleId:(NSString *)bundleId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m new file mode 100644 index 0000000..a8679a8 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBUnattachedAppLauncher.h" + +#import + +#import "LSApplicationWorkspace.h" + +@implementation FBUnattachedAppLauncher + ++ (BOOL)launchAppWithBundleId:(NSString *)bundleId { + return [[LSApplicationWorkspace defaultWorkspace] openApplicationWithBundleID:bundleId]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h new file mode 100644 index 0000000..3add9b1 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h @@ -0,0 +1,42 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Extracts value property for a key action + * + * @param actionItem Action item dictionary + * @param error Contains the acttual error in case of failure + * @returns Either the extracted value or nil in case of failure + */ +NSString *_Nullable FBRequireValue(NSDictionary *actionItem, NSError **error); + +/** + * Extracts duration property for an action + * + * @param actionItem Action item dictionary + * @param defaultValue The default duration value if it is not present. If nil then the error will be set + * @param error Contains the acttual error in case of failure + * @returns Either the extracted value or nil in case of failure + */ +NSNumber *_Nullable FBOptDuration(NSDictionary *actionItem, NSNumber *_Nullable defaultValue, NSError **error); + +/** + * Maps W3C meta modifier to XCUITest compatible-one + * See https://w3c.github.io/webdriver/#keyboard-actions + * + * @param value key action value + * @returns the mapped modifier value or the same input character + * if no mapped value could be found for it. + */ +NSString * FBMapIfSpecialCharacter(NSString *value); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m new file mode 100644 index 0000000..f7a052d --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m @@ -0,0 +1,119 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "FBW3CActionsHelpers.h" + +#import "FBErrorBuilder.h" +#import "XCUIElement.h" +#import "FBLogger.h" + +static NSString *const FB_ACTION_ITEM_KEY_VALUE = @"value"; +static NSString *const FB_ACTION_ITEM_KEY_DURATION = @"duration"; + +NSString *FBRequireValue(NSDictionary *actionItem, NSError **error) +{ + id value = [actionItem objectForKey:FB_ACTION_ITEM_KEY_VALUE]; + if (![value isKindOfClass:NSString.class] || [value length] == 0) { + NSString *description = [NSString stringWithFormat:@"Key value must be present and should be a valid non-empty string for '%@'", actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + NSRange r = [(NSString *)value rangeOfComposedCharacterSequenceAtIndex:0]; + return [(NSString *)value substringWithRange:r]; +} + +NSNumber *_Nullable FBOptDuration(NSDictionary *actionItem, NSNumber *defaultValue, NSError **error) +{ + NSNumber *durationObj = [actionItem objectForKey:FB_ACTION_ITEM_KEY_DURATION]; + if (nil == durationObj) { + if (nil == defaultValue) { + NSString *description = [NSString stringWithFormat:@"Duration must be present for '%@' action item", actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + return defaultValue; + } + if ([durationObj doubleValue] < 0.0) { + NSString *description = [NSString stringWithFormat:@"Duration must be a valid positive number for '%@' action item", actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + return durationObj; +} + +NSString *FBMapIfSpecialCharacter(NSString *value) +{ + if (0 == [value length]) { + return value; + } + + unichar charCode = [value characterAtIndex:0]; + switch (charCode) { + case 0xE000: + return @""; + case 0xE003: + return [NSString stringWithFormat:@"%C", 0x0008]; + case 0xE004: + return [NSString stringWithFormat:@"%C", 0x0009]; + case 0xE006: + return [NSString stringWithFormat:@"%C", 0x000D]; + case 0xE007: + return [NSString stringWithFormat:@"%C", 0x000A]; + case 0xE00C: + return [NSString stringWithFormat:@"%C", 0x001B]; + case 0xE00D: + case 0xE05D: + return @" "; + case 0xE017: + return [NSString stringWithFormat:@"%C", 0x007F]; + case 0xE018: + return @";"; + case 0xE019: + return @"="; + case 0xE01A: + return @"0"; + case 0xE01B: + return @"1"; + case 0xE01C: + return @"2"; + case 0xE01D: + return @"3"; + case 0xE01E: + return @"4"; + case 0xE01F: + return @"5"; + case 0xE020: + return @"6"; + case 0xE021: + return @"7"; + case 0xE022: + return @"8"; + case 0xE023: + return @"9"; + case 0xE024: + return @"*"; + case 0xE025: + return @"+"; + case 0xE026: + return @","; + case 0xE027: + return @"-"; + case 0xE028: + return @"."; + case 0xE029: + return @"/"; + default: + return charCode >= 0xE000 && charCode <= 0xE05D ? @"" : value; + } +} diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h new file mode 100644 index 0000000..746c38e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBBaseActionsSynthesizer.h" + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_TV +@interface FBW3CActionsSynthesizer : FBBaseActionsSynthesizer + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m new file mode 100644 index 0000000..ef17e03 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m @@ -0,0 +1,885 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBW3CActionsSynthesizer.h" + +#import "FBErrorBuilder.h" +#import "FBElementCache.h" +#import "FBConfiguration.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBMathUtils.h" +#import "FBProtocolHelpers.h" +#import "FBW3CActionsHelpers.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement.h" +#import "XCSynthesizedEventRecord.h" +#import "XCPointerEventPath.h" +#import "XCPointerEvent.h" + + +static NSString *const FB_KEY_TYPE = @"type"; +static NSString *const FB_ACTION_TYPE_POINTER = @"pointer"; +static NSString *const FB_ACTION_TYPE_KEY = @"key"; +static NSString *const FB_ACTION_TYPE_NONE = @"none"; + +static NSString *const FB_PARAMETERS_KEY_POINTER_TYPE = @"pointerType"; +static NSString *const FB_POINTER_TYPE_MOUSE = @"mouse"; +static NSString *const FB_POINTER_TYPE_PEN = @"pen"; +static NSString *const FB_POINTER_TYPE_TOUCH = @"touch"; + +static NSString *const FB_ACTION_ITEM_KEY_ORIGIN = @"origin"; +static NSString *const FB_ORIGIN_TYPE_VIEWPORT = @"viewport"; +static NSString *const FB_ORIGIN_TYPE_POINTER = @"pointer"; + +static NSString *const FB_ACTION_ITEM_KEY_TYPE = @"type"; +static NSString *const FB_ACTION_ITEM_TYPE_POINTER_MOVE = @"pointerMove"; +static NSString *const FB_ACTION_ITEM_TYPE_POINTER_DOWN = @"pointerDown"; +static NSString *const FB_ACTION_ITEM_TYPE_POINTER_UP = @"pointerUp"; +static NSString *const FB_ACTION_ITEM_TYPE_POINTER_CANCEL = @"pointerCancel"; +static NSString *const FB_ACTION_ITEM_TYPE_PAUSE = @"pause"; +static NSString *const FB_ACTION_ITEM_TYPE_KEY_UP = @"keyUp"; +static NSString *const FB_ACTION_ITEM_TYPE_KEY_DOWN = @"keyDown"; + +static NSString *const FB_ACTION_ITEM_KEY_X = @"x"; +static NSString *const FB_ACTION_ITEM_KEY_Y = @"y"; +static NSString *const FB_ACTION_ITEM_KEY_BUTTON = @"button"; +static NSString *const FB_ACTION_ITEM_KEY_PRESSURE = @"pressure"; + +static NSString *const FB_KEY_ID = @"id"; +static NSString *const FB_KEY_PARAMETERS = @"parameters"; +static NSString *const FB_KEY_ACTIONS = @"actions"; + + +#if !TARGET_OS_TV +@interface FBW3CGestureItem : FBBaseGestureItem + +@property (nullable, readonly, nonatomic) FBBaseGestureItem *previousItem; + +@end + +@interface FBPointerDownItem : FBW3CGestureItem +@property (nullable, readonly, nonatomic) NSNumber *pressure; +@end + +@interface FBPointerMoveItem : FBW3CGestureItem + +@end + +@interface FBPointerUpItem : FBW3CGestureItem + +@end + +@interface FBPointerPauseItem : FBW3CGestureItem + +@end + + +@interface FBW3CKeyItem : FBBaseActionItem + +@property (nullable, readonly, nonatomic) FBW3CKeyItem *previousItem; + +@end + +@interface FBKeyUpItem : FBW3CKeyItem + +@property (readonly, nonatomic) NSString *value; + +@end + +@interface FBKeyDownItem : FBW3CKeyItem + +@property (readonly, nonatomic) NSString *value; + +@end + +@interface FBKeyPauseItem : FBW3CKeyItem + +@property (readonly, nonatomic) double duration; + +@end + + + +@implementation FBW3CGestureItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBBaseGestureItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super init]; + if (self) { + self.actionItem = actionItem; + self.application = application; + self.offset = offset; + _previousItem = previousItem; + NSNumber *durationObj = FBOptDuration(actionItem, @0, error); + if (nil == durationObj) { + return nil; + } + self.duration = durationObj.doubleValue; + XCUICoordinate *position = [self positionWithError:error]; + if (nil == position) { + return nil; + } + self.atPosition = position; + } + return self; +} + +- (nullable XCUICoordinate *)positionWithError:(NSError **)error +{ + if (nil == self.previousItem) { + NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:errorDescription] build]; + } + return nil; + } + return self.previousItem.atPosition; +} + +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error +{ + if (nil == element || nil == positionOffset) { + return [super hitpointWithElement:element positionOffset:positionOffset error:error]; + } + + // An offset relative to the element is defined + if (CGRectIsEmpty(element.frame)) { + [FBLogger log:self.application.fb_descriptionRepresentation]; + NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", + element.description]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + // W3C standard requires that relative element coordinates start at the center of the element's rectangle + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); + // TODO: Shall we throw an exception if hitPoint is out of the element frame? + return [[element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] coordinateWithOffset:offset]; +} + +@end + +@implementation FBPointerDownItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBW3CGestureItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super initWithActionItem:actionItem application:application previousItem:previousItem offset:offset error:error]; + if (self) { + _pressure = [actionItem objectForKey:FB_ACTION_ITEM_KEY_PRESSURE]; + } + return self; +} + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_POINTER_DOWN; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + if (nil != eventPath && currentItemIndex == 1) { + FBW3CGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1]; + if ([preceedingItem isKindOfClass:FBPointerMoveItem.class]) { + return @[]; + } + } + if (nil == self.pressure) { + XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint + offset:FBMillisToSeconds(self.offset)]; + return @[result]; + } + + if (nil == eventPath) { + NSString *description = [NSString stringWithFormat:@"'%@' action with pressure must be preceeded with at least one '%@' action without this option", self.class.actionName, self.class.actionName]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + if (![XCUIDevice sharedDevice].supportsPressureInteraction) { + if (error) { + *error = [[FBErrorBuilder.builder withDescription:@"This device does not support force press interactions"] build]; + } + return nil; + } + [eventPath pressDownWithPressure:self.pressure.doubleValue + atOffset:FBMillisToSeconds(self.offset)]; + return @[]; +} + +@end + +@implementation FBPointerMoveItem + +- (nullable XCUICoordinate *)positionWithError:(NSError **)error +{ + static NSArray *supportedOriginTypes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + supportedOriginTypes = @[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT]; + }); + id origin = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN] ?: FB_ORIGIN_TYPE_VIEWPORT; + BOOL isOriginAnElement = [origin isKindOfClass:XCUIElement.class] && [(XCUIElement *)origin exists]; + if (!isOriginAnElement && ![supportedOriginTypes containsObject:origin]) { + NSString *description = [NSString stringWithFormat:@"Unsupported %@ type '%@' is set for '%@' action item. Supported origin types: %@ or an element instance", FB_ACTION_ITEM_KEY_ORIGIN, origin, self.actionItem, supportedOriginTypes]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + XCUIElement *element = isOriginAnElement ? (XCUIElement *)origin : nil; + NSNumber *x = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_X]; + NSNumber *y = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_Y]; + if ((nil != x && nil == y) || (nil != y && nil == x) || + ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT] && (nil == x || nil == y))) { + NSString *errorDescription = [NSString stringWithFormat:@"Both 'x' and 'y' options should be set for '%@' action item", self.actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:errorDescription] build]; + } + return nil; + } + + if (nil != element) { + if (nil == x && nil == y) { + return [self hitpointWithElement:element positionOffset:nil error:error]; + } + return [self hitpointWithElement:element positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error]; + } + + if ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT]) { + return [self hitpointWithElement:nil positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error]; + } + + // origin == FB_ORIGIN_TYPE_POINTER + if (nil == self.previousItem) { + NSString *errorDescription = [NSString stringWithFormat:@"There is no previous item for '%@' action item, however %@ is set to '%@'", self.actionItem, FB_ACTION_ITEM_KEY_ORIGIN, FB_ORIGIN_TYPE_POINTER]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:errorDescription] build]; + } + return nil; + } + XCUICoordinate *recentPosition = self.previousItem.atPosition; + CGVector offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGVectorMake(0, 0) : CGVectorMake(x.floatValue, y.floatValue); + return [recentPosition coordinateWithOffset:offsetRelativeToRecentPosition]; +} + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_POINTER_MOVE; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + if (nil == eventPath) { + return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint + offset:FBMillisToSeconds(self.offset + self.duration)]]; + } + [eventPath moveToPoint:self.atPosition.screenPoint + atOffset:FBMillisToSeconds(self.offset + self.duration)]; + return @[]; +} + +@end + +@implementation FBPointerPauseItem + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_PAUSE; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + return @[]; +} + +@end + +@implementation FBPointerUpItem + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_POINTER_UP; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + if (nil == eventPath) { + NSString *description = [NSString stringWithFormat:@"Pointer Up must not be the first action in '%@'", self.actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + [eventPath liftUpAtOffset:FBMillisToSeconds(self.offset)]; + return @[]; +} + +@end + +@implementation FBW3CKeyItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBW3CKeyItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super init]; + if (self) { + self.actionItem = actionItem; + self.application = application; + self.offset = offset; + _previousItem = previousItem; + } + return self; +} + +@end + + +@implementation FBKeyUpItem : FBW3CKeyItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBW3CKeyItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super initWithActionItem:actionItem + application:application + previousItem:previousItem + offset:offset + error:error]; + if (self) { + NSString *value = FBRequireValue(actionItem, error); + if (nil == value) { + return nil; + } + _value = value; + } + return self; +} + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_KEY_UP; +} + +- (BOOL)hasDownPairInItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex +{ + NSInteger balance = 1; + for (NSInteger index = currentItemIndex - 1; index >= 0; index--) { + FBW3CKeyItem *item = [allItems objectAtIndex:index]; + BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; + BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; + if (!isKeyUp && !isKeyDown) { + break; + } + + NSString *value = [item performSelector:@selector(value)]; + if (isKeyDown && [value isEqualToString:self.value]) { + balance--; + } + if (isKeyUp && [value isEqualToString:self.value]) { + balance++; + } + } + return 0 == balance; +} + +- (NSString *)collectTextWithItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex +{ + NSMutableArray *result = [NSMutableArray array]; + for (NSInteger index = currentItemIndex; index >= 0; index--) { + FBW3CKeyItem *item = [allItems objectAtIndex:index]; + BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; + BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; + if (!isKeyUp && !isKeyDown) { + break; + } + + NSString *value = [item performSelector:@selector(value)]; + if (isKeyUp) { + [result addObject:FBMapIfSpecialCharacter(value)]; + } + } + return [result.reverseObjectEnumerator.allObjects componentsJoinedByString:@""]; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + if (![self hasDownPairInItems:allItems currentItemIndex:currentItemIndex]) { + NSString *description = [NSString stringWithFormat:@"Key Up action '%@' is not balanced with a preceding Key Down one in '%@'", self.value, self.actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + BOOL isLastKeyUpInGroup = currentItemIndex == allItems.count - 1 + || [[allItems objectAtIndex:currentItemIndex + 1] isKindOfClass:FBKeyPauseItem.class]; + if (!isLastKeyUpInGroup) { + return @[]; + } + + NSString *text = [self collectTextWithItems:allItems currentItemIndex:currentItemIndex]; + NSTimeInterval offset = FBMillisToSeconds(self.offset); + XCPointerEventPath *resultPath = [[XCPointerEventPath alloc] initForTextInput]; + [resultPath typeText:text + atOffset:offset + typingSpeed:FBConfiguration.maxTypingFrequency + shouldRedact:YES]; + return @[resultPath]; +} + +@end + +@implementation FBKeyDownItem : FBW3CKeyItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBW3CKeyItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super initWithActionItem:actionItem + application:application + previousItem:previousItem + offset:offset + error:error]; + if (self) { + NSString *value = FBRequireValue(actionItem, error); + if (nil == value) { + return nil; + } + _value = value; + } + return self; +} + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_KEY_DOWN; +} + +- (BOOL)hasUpPairInItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex +{ + NSInteger balance = 1; + for (NSUInteger index = currentItemIndex + 1; index < allItems.count; index++) { + FBW3CKeyItem *item = [allItems objectAtIndex:index]; + BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; + BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; + if (!isKeyUp && !isKeyDown) { + break; + } + + NSString *value = [item performSelector:@selector(value)]; + if (isKeyUp && [value isEqualToString:self.value]) { + balance--; + } + if (isKeyDown && [value isEqualToString:self.value]) { + balance++; + } + } + return 0 == balance; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + if (![self hasUpPairInItems:allItems currentItemIndex:currentItemIndex]) { + NSString *description = [NSString stringWithFormat:@"Key Down action '%@' must have a closing Key Up successor in '%@'", self.value, self.actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + return @[]; +} + +@end + +@implementation FBKeyPauseItem + +- (nullable instancetype)initWithActionItem:(NSDictionary *)actionItem + application:(XCUIApplication *)application + previousItem:(nullable FBW3CKeyItem *)previousItem + offset:(double)offset + error:(NSError **)error +{ + self = [super initWithActionItem:actionItem + application:application + previousItem:previousItem + offset:offset + error:error]; + if (self) { + NSNumber *duration = FBOptDuration(actionItem, nil, error); + if (nil == duration) { + return nil; + } + _duration = [duration doubleValue]; + } + return self; +} + ++ (NSString *)actionName +{ + return FB_ACTION_ITEM_TYPE_PAUSE; +} + +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error +{ + return @[]; +} + +@end + + +@interface FBW3CGestureItemsChain : FBBaseActionItemsChain + +@end + +@implementation FBW3CGestureItemsChain + +- (void)addItem:(FBBaseActionItem *)item +{ + self.durationOffset += ((FBBaseGestureItem *)item).duration; + [self.items addObject:item]; +} + +@end + + +@interface FBW3CKeyItemsChain : FBBaseActionItemsChain + +@end + +@implementation FBW3CKeyItemsChain + +- (void)addItem:(FBBaseActionItem *)item +{ + if ([item isKindOfClass:FBKeyPauseItem.class]) { + self.durationOffset += ((FBKeyPauseItem *)item).duration; + } + [self.items addObject:item]; +} + +@end + + +@implementation FBW3CActionsSynthesizer + +- (NSArray *> *)preprocessedActionItemsWith:(NSArray *> *)actionItems +{ + NSMutableArray *> *result = [NSMutableArray array]; + BOOL shouldCancelNextItem = NO; + for (NSDictionary *actionItem in [actionItems reverseObjectEnumerator]) { + if (shouldCancelNextItem) { + shouldCancelNextItem = NO; + continue; + } + NSString *actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE]; + if (actionItemType != nil && [actionItemType isEqualToString:FB_ACTION_ITEM_TYPE_POINTER_CANCEL]) { + shouldCancelNextItem = YES; + continue; + } + + if (nil == self.elementCache) { + [result addObject:actionItem]; + continue; + } + id origin = [actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN]; + if (nil == origin || [@[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT] containsObject:origin]) { + [result addObject:actionItem]; + continue; + } + // Selenium Python client passes 'origin' element in the following format: + // + // if isinstance(origin, WebElement): + // action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id} + if ([origin isKindOfClass:NSDictionary.class]) { + id element = FBExtractElement(origin); + if (nil != element) { + origin = element; + } + } + + XCUIElement *instance; + if ([origin isKindOfClass:XCUIElement.class]) { + instance = origin; + } else if ([origin isKindOfClass:NSString.class]) { + instance = [self.elementCache elementForUUID:(NSString *)origin checkStaleness:YES]; + } else { + [result addObject:actionItem]; + continue; + } + NSMutableDictionary *processedItem = actionItem.mutableCopy; + [processedItem setObject:instance forKey:FB_ACTION_ITEM_KEY_ORIGIN]; + [result addObject:processedItem.copy]; + } + return [[result reverseObjectEnumerator] allObjects]; +} + +- (nullable NSArray *)eventPathsWithKeyAction:(NSDictionary *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error +{ + static NSDictionary *keyItemsMapping; + static NSArray *supportedActionItemTypes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *itemsMapping = [NSMutableDictionary dictionary]; + for (Class cls in @[FBKeyDownItem.class, + FBKeyPauseItem.class, + FBKeyUpItem.class]) { + [itemsMapping setObject:cls forKey:[cls actionName]]; + } + keyItemsMapping = itemsMapping.copy; + supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE, + FB_ACTION_ITEM_TYPE_KEY_UP, + FB_ACTION_ITEM_TYPE_KEY_DOWN]; + }); + + NSArray *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS]; + if (nil == actionItems || 0 == actionItems.count) { + NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + FBW3CKeyItemsChain *chain = [[FBW3CKeyItemsChain alloc] init]; + NSArray *> *processedItems = [self preprocessedActionItemsWith:actionItems]; + for (NSDictionary *actionItem in processedItems) { + id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE]; + if (![actionItemType isKindOfClass:NSString.class]) { + NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + Class keyItemClass = [keyItemsMapping objectForKey:actionItemType]; + if (nil == keyItemClass) { + NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + FBW3CKeyItem *keyItem = [[keyItemClass alloc] initWithActionItem:actionItem + application:self.application + previousItem:[chain.items lastObject] + offset:chain.durationOffset + error:error]; + if (nil == keyItem) { + return nil; + } + + [chain addItem:keyItem]; + } + + return [chain asEventPathsWithError:error]; +} + +- (nullable NSArray *)eventPathsWithGestureAction:(NSDictionary *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error +{ + static NSDictionary *gestureItemsMapping; + static NSArray *supportedActionItemTypes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *itemsMapping = [NSMutableDictionary dictionary]; + for (Class cls in @[FBPointerDownItem.class, + FBPointerMoveItem.class, + FBPointerPauseItem.class, + FBPointerUpItem.class]) { + [itemsMapping setObject:cls forKey:[cls actionName]]; + } + gestureItemsMapping = itemsMapping.copy; + supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE, + FB_ACTION_ITEM_TYPE_POINTER_UP, + FB_ACTION_ITEM_TYPE_POINTER_DOWN, + FB_ACTION_ITEM_TYPE_POINTER_MOVE]; + }); + + id parameters = [actionDescription objectForKey:FB_KEY_PARAMETERS]; + id pointerType = FB_POINTER_TYPE_MOUSE; + if ([parameters isKindOfClass:NSDictionary.class]) { + pointerType = [parameters objectForKey:FB_PARAMETERS_KEY_POINTER_TYPE] ?: FB_POINTER_TYPE_MOUSE; + } + if (![pointerType isKindOfClass:NSString.class] || ![pointerType isEqualToString:FB_POINTER_TYPE_TOUCH]) { + NSString *description = [NSString stringWithFormat:@"Only pointer type '%@' is supported. '%@' is given instead for action with id '%@'", FB_POINTER_TYPE_TOUCH, pointerType, actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + NSArray *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS]; + if (nil == actionItems || 0 == actionItems.count) { + NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + FBW3CGestureItemsChain *chain = [[FBW3CGestureItemsChain alloc] init]; + NSArray *> *processedItems = [self preprocessedActionItemsWith:actionItems]; + for (NSDictionary *actionItem in processedItems) { + id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE]; + if (![actionItemType isKindOfClass:NSString.class]) { + NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + Class gestureItemClass = [gestureItemsMapping objectForKey:actionItemType]; + if (nil == gestureItemClass) { + NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + FBW3CGestureItem *gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application previousItem:[chain.items lastObject] offset:chain.durationOffset error:error]; + if (nil == gestureItem) { + return nil; + } + + [chain addItem:gestureItem]; + } + + return [chain asEventPathsWithError:error]; +} + +- (nullable NSArray *)eventPathsWithActionDescription:(NSDictionary *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error +{ + id actionType = [actionDescription objectForKey:FB_KEY_TYPE]; + if (![actionType isKindOfClass:NSString.class] || + !([actionType isEqualToString:FB_ACTION_TYPE_POINTER] + || ([XCPointerEvent.class fb_areKeyEventsSupported] && [actionType isEqualToString:FB_ACTION_TYPE_KEY]))) { + NSString *description = [NSString stringWithFormat:@"Only actions of '%@' types are supported. '%@' is given instead for action with id '%@'", @[FB_ACTION_TYPE_POINTER, FB_ACTION_TYPE_KEY], actionType, actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + + if ([actionType isEqualToString:FB_ACTION_TYPE_POINTER]) { + return [self eventPathsWithGestureAction:actionDescription forActionId:actionId error:error]; + } + + return [self eventPathsWithKeyAction:actionDescription forActionId:actionId error:error]; +} + +- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error +{ + XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] + initWithName:@"W3C Touch Action" + interfaceOrientation:self.application.interfaceOrientation]; + NSMutableDictionary *> *actionsMapping = [NSMutableDictionary new]; + NSMutableArray *actionIds = [NSMutableArray new]; + for (NSDictionary *action in self.actions) { + id actionId = [action objectForKey:FB_KEY_ID]; + if (![actionId isKindOfClass:NSString.class] || 0 == [actionId length]) { + if (error) { + NSString *description = [NSString stringWithFormat:@"The mandatory action %@ field is missing or empty for '%@'", FB_KEY_ID, action]; + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + if (nil != [actionsMapping objectForKey:actionId]) { + if (error) { + NSString *description = [NSString stringWithFormat:@"Action %@ '%@' is not unique for '%@'", FB_KEY_ID, actionId, action]; + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + NSArray *> *actionItems = [action objectForKey:FB_KEY_ACTIONS]; + if (nil == actionItems) { + NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + if (0 == actionItems.count) { + [FBLogger logFmt:@"Action items in the action id '%@' had an empty array. Skipping the action.", actionId]; + continue; + } + + [actionIds addObject:actionId]; + [actionsMapping setObject:action forKey:actionId]; + } + for (NSString *actionId in actionIds.copy) { + NSDictionary *actionDescription = [actionsMapping objectForKey:actionId]; + NSArray *eventPaths = [self eventPathsWithActionDescription:actionDescription forActionId:actionId error:error]; + if (nil == eventPaths) { + return nil; + } + for (XCPointerEventPath *eventPath in eventPaths) { + [eventRecord addPointerEventPath:eventPath]; + } + } + return eventRecord; +} + +@end +#endif diff --git a/WebDriverAgentLib/Utilities/FBWebServerParams.h b/WebDriverAgentLib/Utilities/FBWebServerParams.h new file mode 100644 index 0000000..aba0fa3 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBWebServerParams.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBWebServerParams : NSObject + +/** The local port number WDA server is running on */ +@property (nonatomic, nullable) NSNumber *port; + ++ (id)sharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBWebServerParams.m b/WebDriverAgentLib/Utilities/FBWebServerParams.m new file mode 100644 index 0000000..9ced0a3 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBWebServerParams.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBWebServerParams.h" + +@implementation FBWebServerParams + ++ (instancetype)sharedInstance +{ + static FBWebServerParams *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h new file mode 100644 index 0000000..b216e30 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "FBXCElementSnapshot.h" + +@protocol FBXCAccessibilityElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + This class acts as a proxy between WDA and XCAXClient_iOS. + Other classes are obliged to use its methods instead of directly accessing XCAXClient_iOS, + since Apple resticted the interface of XCAXClient_iOS class since Xcode10.2 + */ +@interface FBXCAXClientProxy : NSObject + ++ (instancetype)sharedClient; + +- (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error; + +- (nullable id)snapshotForElement:(id)element + attributes:(nullable NSArray *)attributes + inDepth:(BOOL)inDepth + error:(NSError **)error; + +- (NSArray> *)activeApplications; + +- (id)systemApplication; + +- (NSDictionary *)defaultParameters; + +- (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application + reply:(void (^)(void))reply; + +- (nullable NSDictionary *)attributesForElement:(id)element + attributes:(NSArray *)attributes + error:(NSError**)error; + +- (nullable XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m new file mode 100644 index 0000000..f84f803 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCAXClientProxy.h" + +#import "FBXCAccessibilityElement.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "XCAXClient_iOS+FBSnapshotReqParams.h" +#import "XCUIDevice.h" +#import "XCUIApplication.h" + +static id FBAXClient = nil; + +@interface FBXCAXClientProxy () + +@property (nonatomic) NSMutableDictionary *appsCache; + +@end + +@implementation FBXCAXClientProxy + ++ (instancetype)sharedClient +{ + static FBXCAXClientProxy *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + instance.appsCache = [NSMutableDictionary dictionary]; + FBAXClient = [XCUIDevice.sharedDevice accessibilityInterface]; + }); + return instance; +} + +- (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error +{ + return [FBAXClient _setAXTimeout:timeout error:error]; +} + +- (id)snapshotForElement:(id)element + attributes:(NSArray *)attributes + inDepth:(BOOL)inDepth + error:(NSError **)error +{ + NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.defaultParameters]; + if (!inDepth) { + parameters[FBSnapshotMaxDepthKey] = @1; + } + + id result = [FBAXClient requestSnapshotForElement:element + attributes:attributes + parameters:[parameters copy] + error:error]; + id snapshot = [result valueForKey:@"_rootElementSnapshot"]; + return nil == snapshot ? result : snapshot; +} + +- (NSArray> *)activeApplications +{ + return [FBAXClient activeApplications]; +} + +- (id)systemApplication +{ + return [FBAXClient systemApplication]; +} + +- (NSDictionary *)defaultParameters +{ + return [FBAXClient defaultParameters]; +} + +- (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application + reply:(void (^)(void))reply +{ + [FBAXClient notifyWhenNoAnimationsAreActiveForApplication:application reply:reply]; +} + +- (NSDictionary *)attributesForElement:(id)element + attributes:(NSArray *)attributes + error:(NSError**)error; +{ + return [FBAXClient attributesForElement:element + attributes:attributes + error:error]; +} + +- (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid +{ + NSMutableSet *terminatedAppIds = [NSMutableSet set]; + for (NSNumber *appPid in self.appsCache) { + if (![self.appsCache[appPid] running]) { + [terminatedAppIds addObject:appPid]; + } + } + for (NSNumber *appPid in terminatedAppIds) { + [self.appsCache removeObjectForKey:appPid]; + } + + XCUIApplication *result = [self.appsCache objectForKey:@(pid)]; + if (nil != result) { + return result; + } + + XCUIApplication *app = [[FBAXClient applicationProcessTracker] + monitoredApplicationWithProcessIdentifier:pid]; + if (nil == app) { + return nil; + } + [self.appsCache setObject:app forKey:@(pid)]; + return app; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h new file mode 100644 index 0000000..41514a5 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#if !TARGET_OS_TV +#import +#endif + +#import "XCSynthesizedEventRecord.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol XCTestManager_ManagerInterface; +@class FBScreenRecordingRequest, FBScreenRecordingPromise; + +@interface FBXCTestDaemonsProxy : NSObject + ++ (id)testRunnerProxy; + ++ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record + error:(NSError *__autoreleasing*)error; + ++ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError **)error; ++ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError **)error; + ++ (nullable FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request + error:(NSError **)error; ++ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid + error:(NSError **)error; + +#if !TARGET_OS_TV ++ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError **)error; ++ (nullable CLLocation *)getSimulatedLocation:(NSError **)error; ++ (BOOL)clearSimulatedLocation:(NSError **)error; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m new file mode 100644 index 0000000..e29b94e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -0,0 +1,359 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCTestDaemonsProxy.h" + +#import + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBExceptions.h" +#import "FBLogger.h" +#import "FBRunLoopSpinner.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" +#import "XCTestDriver.h" +#import "XCTRunnerDaemonSession.h" +#import "XCUIApplication.h" +#import "XCUIDevice.h" + +#define LAUNCH_APP_TIMEOUT_SEC 300 + +static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *)); + +static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID, + NSArray *arguments, NSDictionary *environment, + void (^reply)(_Bool, NSError *)) +{ + __block BOOL isSuccessful; + __block NSError *error; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) { + isSuccessful = passed; + error = innerError; + dispatch_semaphore_signal(sem); + }); + int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { + NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout", + bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC]; + @throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil]; + } + if (!isSuccessful || nil != error) { + [FBLogger logFmt:@"%@", error.description]; + NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test", + bundleID ?: path]; + @throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil]; + } + reply(isSuccessful, error); +} + +@implementation FBXCTestDaemonsProxy + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + ++ (void)load +{ + [self.class swizzleLaunchApp]; +} + +#pragma clang diagnostic pop + ++ (void)swizzleLaunchApp { + Method original = class_getInstanceMethod([XCTRunnerDaemonSession class], + @selector(launchApplicationWithPath:bundleID:arguments:environment:completion:)); + if (original == nil) { + [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"]; + return; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + // Workaround for https://github.com/appium/WebDriverAgent/issues/702 + originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original); + method_setImplementation(original, (IMP)swizzledLaunchApp); +#pragma clang diagnostic pop +} + ++ (id)testRunnerProxy +{ + static id proxy = nil; + if ([FBConfiguration shouldUseSingletonTestManager]) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [FBLogger logFmt:@"Using singleton test manager"]; + proxy = [self.class retrieveTestRunnerProxy]; + }); + } else { + [FBLogger logFmt:@"Using general test manager"]; + proxy = [self.class retrieveTestRunnerProxy]; + } + NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy); + return proxy; +} + ++ (id)retrieveTestRunnerProxy +{ + return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy; +} + ++ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error +{ + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + void (^errorHandler)(NSError *) = ^(NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } + completion(); + }; + + XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) { + errorHandler(invokeError); + }; + [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) { + handlerBlock(record, invokeError); + }]; + }]; + if (nil != innerError) { + if (error) { + *error = innerError; + } + return NO; + } + return YES; +} + ++ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"] + buildError:error]; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} + ++ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} + +#if !TARGET_OS_TV ++ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + } + if (![session supportsLocationSimulation]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} + ++ (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error; +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + return nil; + } + if (![session supportsLocationSimulation]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + return nil; + } + + __block NSError *innerError = nil; + __block CLLocation *location = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + location = reply; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return location; +} + ++ (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + } + if (![session supportsLocationSimulation]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} +#endif + ++ (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request + error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + buildError:error]; + return nil; + } + if (![session supportsScreenRecording]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support screen recording"] + buildError:error]; + return nil; + } + + id nativeRequest = [request toNativeRequestWithError:error]; + if (nil == nativeRequest) { + return nil; + } + + __block id futureMetadata = nil; + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) { + if (nil == invokeError) { + futureMetadata = reply; + } else { + innerError = invokeError; + } + completion(); + }]; + }]; + if (nil != innerError) { + if (error) { + *error = innerError; + } + return nil; + } + return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata]; +} + ++ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + buildError:error]; + + } + if (![session supportsScreenRecording]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support screen recording"] + buildError:error]; + } + + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return nil == innerError; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h new file mode 100644 index 0000000..75d1ee2 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "XCPointerEvent.h" + +@class FBXCElementSnapshot; + +/** + The version of testmanagerd process which is running on the device. + + Potentially, we can handle processes based on this version instead of iOS versions, + iOS 10.1 -> 6 + iOS 11.0.1 -> 18 + iOS 11.4 -> 22 + iOS 12.1, 12.4 -> 26 + iOS 13.0, 13.4.1 -> 28 + + tvOS 13.3 -> 28 + + @return The version of testmanagerd + */ +NSInteger FBTestmanagerdVersion(void); + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElementQuery (FBCompatibility) + +/* Performs short-circuit UI tree traversion in iOS 11+ to get the first element matched by the query. Equals to nil if no matching elements are found */ +@property(nullable, readonly) XCUIElement *fb_firstMatch; + +/* + This is the local wrapper for bounded elements extraction. + It uses either indexed or bounded binding based on the `boundElementsByIndex` configuration + flag value. + */ +@property(readonly) NSArray *fb_allMatches; + +/** + Returns single unique matching snapshot for the given query + + @param error The error instance if there was a failure while retrieveing the snapshot + @returns The cached unqiue snapshot or nil if the element is stale + */ +- (nullable id)fb_uniqueSnapshotWithError:(NSError **)error; + +@end + + +@interface XCPointerEvent (FBCompatibility) + +- (BOOL)fb_areKeyEventsSupported; + +@end + + +@interface XCUIElement (FBCompatibility) + +/** + Determines whether current iOS SDK supports non modal elements inlusion into snapshots + + @return Either YES or NO + */ ++ (BOOL)fb_supportsNonModalElementsInclusion; + +/** + Retrieves element query + + @return Element query property extended with non modal elements depending on the actual configuration + */ +- (XCUIElementQuery *)fb_query; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m new file mode 100644 index 0000000..f2cf03a --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXCodeCompatibility.h" + +#import "FBXCAXClientProxy.h" +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElementQuery.h" +#import "FBXCTestDaemonsProxy.h" +#import "XCTestManager_ManagerInterface-Protocol.h" + +@implementation XCUIElementQuery (FBCompatibility) + +- (id)fb_uniqueSnapshotWithError:(NSError **)error +{ + return (id)[self uniqueMatchingSnapshotWithError:error]; +} + +- (XCUIElement *)fb_firstMatch +{ + if (FBConfiguration.useFirstMatch) { + XCUIElement* match = self.firstMatch; + return [match exists] ? match : nil; + } + return self.fb_allMatches.firstObject; +} + +- (NSArray *)fb_allMatches +{ + return FBConfiguration.boundElementsByIndex + ? self.allElementsBoundByIndex + : self.allElementsBoundByAccessibilityElement; +} + +@end + + +@implementation XCUIElement (FBCompatibility) + ++ (BOOL)fb_supportsNonModalElementsInclusion +{ + static dispatch_once_t hasIncludingNonModalElements; + static BOOL result; + dispatch_once(&hasIncludingNonModalElements, ^{ + result = [XCUIApplication.fb_systemApplication.query respondsToSelector:@selector(includingNonModalElements)]; + }); + return result; +} + +- (XCUIElementQuery *)fb_query +{ + return FBConfiguration.includeNonModalElements && self.class.fb_supportsNonModalElementsInclusion + ? self.query.includingNonModalElements + : self.query; +} + +@end + +@implementation XCPointerEvent (FBXcodeCompatibility) + ++ (BOOL)fb_areKeyEventsSupported +{ + static BOOL isKbInputSupported = NO; + static dispatch_once_t onceKbInputSupported; + dispatch_once(&onceKbInputSupported, ^{ + isKbInputSupported = [XCPointerEvent.class respondsToSelector:@selector(keyboardEventForKeyCode:keyPhase:modifierFlags:offset:)]; + }); + return isKbInputSupported; +} + +@end + +NSInteger FBTestmanagerdVersion(void) +{ + static dispatch_once_t getTestmanagerdVersion; + static NSInteger testmanagerdVersion; + dispatch_once(&getTestmanagerdVersion, ^{ + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; + if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) { + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [proxy _XCT_exchangeProtocolVersion:testmanagerdVersion reply:^(unsigned long long code) { + testmanagerdVersion = (NSInteger) code; + completion(); + }]; + }]; + } else { + testmanagerdVersion = 0xFFFF; + } + }); + return testmanagerdVersion; +} diff --git a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h new file mode 100644 index 0000000..8a014c1 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXMLGenerationOptions : NSObject + +/** + XML buidling scope. Passing nil means the XML should be built in the default scope, + i.e no changes to the original tree structore. If the scope is provided then the resulting + XML tree will be put under the root, which name is equal to the given scope value. + */ +@property (nonatomic, nullable) NSString *scope; +/** + The list of attribute names to exclude from the resulting document. + Passing nil means all the available attributes should be included + */ +@property (nonatomic, nullable) NSArray *excludedAttributes; + +/** + Allows to provide XML scope. + + @param scope See the property description above + @return self instance for chaining + */ +- (FBXMLGenerationOptions *)withScope:(nullable NSString *)scope; + +/** + Allows to provide a list of excluded XML attributes. + + @param excludedAttributes See the property description above + @return self instance for chaining + */ +- (FBXMLGenerationOptions *)withExcludedAttributes:(nullable NSArray *)excludedAttributes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m new file mode 100644 index 0000000..40dcebd --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXMLGenerationOptions.h" + +@implementation FBXMLGenerationOptions + +- (FBXMLGenerationOptions *)withScope:(NSString *)scope +{ + self.scope = scope; + return self; +} + +- (FBXMLGenerationOptions *)withExcludedAttributes:(NSArray *)excludedAttributes +{ + self.excludedAttributes = excludedAttributes; + return self; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBXPath-Private.h b/WebDriverAgentLib/Utilities/FBXPath-Private.h new file mode 100644 index 0000000..db9732c --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPath-Private.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXPath () + +/** + Gets xmllib2-compatible XML representation of n XCElementSnapshot instance + + @param root the root element to execute XPath query for + @param writer the correspondig libxml2 writer object + @param elementStore an empty dictionary to store indexes mapping or nil if no mappings should be stored + @param query Optional XPath query value. By analyzing this query we may optimize the lookup speed. + @param excludedAttributes The list of XML attribute names to be excluded from the generated XML representation. + Setting nil to this argument means that none of the known attributes must be excluded. + If `query` argument is assigned then `excludedAttributes` argument is effectively ignored. + @return zero if the method has completed successfully + */ ++ (int)xmlRepresentationWithRootElement:(id)root + writer:(xmlTextWriterPtr)writer + elementStore:(nullable NSMutableDictionary *)elementStore + query:(nullable NSString*)query + excludingAttributes:(nullable NSArray *)excludedAttributes; + +/** + Gets the list of matched snapshots from xmllib2-compatible xmlNodeSetPtr structure + + @param nodeSet set of nodes returned after successful XPath evaluation + @param elementStore dictionary containing index->snapshot mapping + @return array of filtered elements or nil in case of failure. Can be empty array as well + */ ++ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet elementStore:(NSMutableDictionary *)elementStore; + +/** + Gets the list of matched XPath nodes from xmllib2-compatible XML document + + @param xpathQuery actual query. Should be valid XPath 1.0-compatible expression + @param document libxml2-compatible document pointer + @param contextNode Optonal context node instance + @return pointer to a libxml2-compatible structure with set of matched nodes or NULL in case of failure + */ ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPath.h b/WebDriverAgentLib/Utilities/FBXPath.h new file mode 100644 index 0000000..1135c72 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPath.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif + +#import +#import +#import +#import +#import +#import + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +@class FBXMLGenerationOptions; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXPath : NSObject + +/** + Returns an array of descendants matching given xpath query + + @param root the root element to execute XPath query for + @param xpathQuery requested xpath query + @return an array of descendants matching the given xpath query or an empty array if no matches were found + @throws NSException if there is an unexpected internal error during xml parsing + */ ++ (NSArray> *)matchesWithRootElement:(id)root + forQuery:(NSString *)xpathQuery; + +/** + Gets XML representation of XCElementSnapshot with all its descendants. This method generates the same + representation, which is used for XPath search + + @param root the root element + @param options Optional values that affect the resulting XML creation process + @return valid XML document as string or nil in case of failure + */ ++ (nullable NSString *)xmlStringWithRootElement:(id)root + options:(nullable FBXMLGenerationOptions *)options; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m new file mode 100644 index 0000000..79e8f75 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -0,0 +1,954 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXPath.h" + +#import "FBConfiguration.h" +#import "FBExceptions.h" +#import "FBElementUtils.h" +#import "FBLogger.h" +#import "FBMacros.h" +#import "FBXMLGenerationOptions.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "NSString+FBXMLSafeString.h" +#import "XCUIApplication.h" +#import "XCUIElement.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCTestPrivateSymbols.h" +#import "FBElementHelpers.h" +#import "FBXCAXClientProxy.h" +#import "FBXCAccessibilityElement.h" + + +@interface FBElementAttribute : NSObject + +@property (nonatomic, readonly) id element; + ++ (nonnull NSString *)name; ++ (nullable NSString *)valueForElement:(id)element; + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value; + ++ (NSArray *)supportedAttributes; + +@end + +@interface FBTypeAttribute : FBElementAttribute + +@end + +@interface FBValueAttribute : FBElementAttribute + +@end + +@interface FBNameAttribute : FBElementAttribute + +@end + +@interface FBLabelAttribute : FBElementAttribute + +@end + +@interface FBEnabledAttribute : FBElementAttribute + +@end + +@interface FBVisibleAttribute : FBElementAttribute + +@end + +@interface FBAccessibleAttribute : FBElementAttribute + +@end + +@interface FBDimensionAttribute : FBElementAttribute + +@end + +@interface FBXAttribute : FBDimensionAttribute + +@end + +@interface FBYAttribute : FBDimensionAttribute + +@end + +@interface FBWidthAttribute : FBDimensionAttribute + +@end + +@interface FBHeightAttribute : FBDimensionAttribute + +@end + +@interface FBIndexAttribute : FBElementAttribute + +@end + +@interface FBHittableAttribute : FBElementAttribute + +@end + +@interface FBInternalIndexAttribute : FBElementAttribute + +@property (nonatomic, nonnull, readonly) NSString* indexValue; + +@end + +@interface FBApplicationBundleIdAttribute : FBElementAttribute + +@end + +@interface FBApplicationPidAttribute : FBElementAttribute + +@end + +@interface FBPlaceholderValueAttribute : FBElementAttribute + +@end + +@interface FBNativeFrameAttribute : FBElementAttribute + +@end + +@interface FBTraitsAttribute : FBElementAttribute + +@end + +@interface FBMinValueAttribute : FBElementAttribute + +@end + +@interface FBMaxValueAttribute : FBElementAttribute + +@end + +#if TARGET_OS_TV + +@interface FBFocusedAttribute : FBElementAttribute + +@end + +#endif + +const static char *_UTF8Encoding = "UTF-8"; + +static NSString *const kXMLIndexPathKey = @"private_indexPath"; +static NSString *const topNodeIndexPath = @"top"; + +@implementation FBXPath + ++ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery +{ + NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; + @throw [NSException exceptionWithName:name reason:reason userInfo:@{}]; + return nil; +} + ++ (nullable NSString *)xmlStringWithRootElement:(id)root + options:(nullable FBXMLGenerationOptions *)options +{ + xmlDocPtr doc; + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; + } else { + BOOL hasScope = nil != options.scope && [options.scope length] > 0; + if (hasScope) { + rc = xmlTextWriterStartElement(writer, + (xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc]; + } + } + + if (rc >= 0) { + [self waitUntilStableWithElement:root]; + // If 'includeHittableInPageSource' setting is enabled, then use native snapshots + // to calculate a more accurate value for the 'hittable' attribute. + rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root + useNative:FBConfiguration.includeHittableInPageSource] + writer:writer + elementStore:nil + query:nil + excludingAttributes:options.excludedAttributes]; + } + + if (rc >= 0 && hasScope) { + rc = xmlTextWriterEndElement(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc]; + } + } + + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc]; + } + } + } + if (rc < 0) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + return nil; + } + int buffersize; + xmlChar *xmlbuff; + xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; + xmlFree(xmlbuff); + return result; +} + ++ (NSArray> *)matchesWithRootElement:(id)root + forQuery:(NSString *)xpathQuery +{ + xmlDocPtr doc; + + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + if (NULL == writer) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery]; + return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; + } + NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; + int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); + id lookupScopeSnapshot = nil; + id contextRootSnapshot = nil; + BOOL useNativeSnapshot = nil == xpathQuery + ? NO + : [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class]; + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; + } else { + [self waitUntilStableWithElement:root]; + if (FBConfiguration.limitXpathContextScope) { + lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot]; + } else { + if ([root isKindOfClass:XCUIElement.class]) { + lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application] + useNative:useNativeSnapshot]; + contextRootSnapshot = [root isKindOfClass:XCUIApplication.class] + ? nil + : ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root + useNative:useNativeSnapshot]); + } else { + lookupScopeSnapshot = (id)root; + contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id)root; + while (nil != lookupScopeSnapshot.parent) { + lookupScopeSnapshot = lookupScopeSnapshot.parent; + } + } + } + + rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot + writer:writer + elementStore:elementStore + query:xpathQuery + excludingAttributes:nil]; + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc]; + } + } + } + if (rc < 0) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; + } + + xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc + elementStore:elementStore.copy + forSnapshot:contextRootSnapshot]; + xmlNodePtr contextNode = NULL; + if (NULL != contextNodeQueryResult) { + xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval; + if (!xmlXPathNodeSetIsEmpty(nodeSet)) { + contextNode = nodeSet->nodeTab[0]; + } + } + xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery + document:doc + contextNode:contextNode]; + if (NULL != contextNodeQueryResult) { + xmlXPathFreeObject(contextNodeQueryResult); + } + if (NULL == queryResult) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + return [self throwException:FBInvalidXPathException forQuery:xpathQuery]; + } + + NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval + elementStore:elementStore]; + xmlXPathFreeObject(queryResult); + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + if (nil == matchingSnapshots) { + return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; + } + return matchingSnapshots; +} + ++ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet + elementStore:(NSMutableDictionary *)elementStore +{ + if (xmlXPathNodeSetIsEmpty(nodeSet)) { + return @[]; + } + NSMutableArray *matchingSnapshots = [NSMutableArray array]; + const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String]; + for (NSInteger i = 0; i < nodeSet->nodeNr; i++) { + xmlNodePtr currentNode = nodeSet->nodeTab[i]; + xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName); + if (NULL == attrValue) { + [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"]; + return nil; + } + id element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]]; + if (element) { + [matchingSnapshots addObject:element]; + } + xmlFree(attrValue); + } + return matchingSnapshots.copy; +} + ++ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc + elementStore:(NSDictionary> *)elementStore + forSnapshot:(nullable id)snapshot +{ + if (nil == snapshot) { + return NULL; + } + + NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]]; + if (nil == contextRootUid) { + return NULL; + } + + for (NSString *key in elementStore) { + id value = [elementStore objectForKey:key]; + NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]]; + if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) { + continue; + } + NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key]; + xmlXPathObjectPtr queryResult = [self evaluate:indexQuery + document:doc + contextNode:NULL]; + if (NULL != queryResult) { + return queryResult; + } + } + return NULL; +} + ++ (NSSet *)elementAttributesWithXPathQuery:(NSString *)query +{ + if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) { + // read all element attributes if 'star' attribute name pattern is used in xpath query + return [NSSet setWithArray:FBElementAttribute.supportedAttributes]; + } + NSMutableSet *result = [NSMutableSet set]; + for (Class attributeCls in FBElementAttribute.supportedAttributes) { + if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) { + [result addObject:attributeCls]; + } + } + return result.copy; +} + ++ (int)xmlRepresentationWithRootElement:(id)root + writer:(xmlTextWriterPtr)writer + elementStore:(nullable NSMutableDictionary *)elementStore + query:(nullable NSString*)query + excludingAttributes:(nullable NSArray *)excludedAttributes +{ + // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document. + // This may speed up the lookup significantly in some cases + NSMutableSet *includedAttributes; + if (nil == query) { + includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes]; + if (!FBConfiguration.includeHittableInPageSource) { + // The hittable attribute is expensive to calculate for each snapshot item + // thus we only include it when requested explicitly + [includedAttributes removeObject:FBHittableAttribute.class]; + } + if (!FBConfiguration.includeNativeFrameInPageSource) { + // Include nativeFrame only when requested + [includedAttributes removeObject:FBNativeFrameAttribute.class]; + } + if (!FBConfiguration.includeMinMaxValueInPageSource) { + // minValue/maxValue are retrieved from private APIs and may be slow on deep trees + [includedAttributes removeObject:FBMinValueAttribute.class]; + [includedAttributes removeObject:FBMaxValueAttribute.class]; + } + if (nil != excludedAttributes) { + for (NSString *excludedAttributeName in excludedAttributes) { + for (Class supportedAttribute in FBElementAttribute.supportedAttributes) { + if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) { + [includedAttributes removeObject:supportedAttribute]; + break; + } + } + } + } + } else { + includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy; + } + [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes]; + + int rc = [self writeXmlWithRootElement:root + indexPath:(elementStore != nil ? topNodeIndexPath : nil) + elementStore:elementStore + includedAttributes:includedAttributes.copy + writer:writer]; + if (rc < 0) { + [FBLogger log:@"Failed to generate XML presentation of a screen element"]; + return rc; + } + return 0; +} + ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode +{ + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); + if (NULL == xpathCtx) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery]; + return NULL; + } + xpathCtx->node = NULL == contextNode ? doc->children : contextNode; + + xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx); + if (NULL == xpathObj) { + xmlXPathFreeContext(xpathCtx); + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; + return NULL; + } + xmlXPathFreeContext(xpathCtx); + return xpathObj; +} + ++ (nullable NSString *)safeXmlStringWithString:(NSString *)str +{ + return [str fb_xmlSafeStringWithReplacement:@""]; +} + ++ (int)recordElementAttributes:(xmlTextWriterPtr)writer + forElement:(id)element + indexPath:(nullable NSString *)indexPath + includedAttributes:(nullable NSSet *)includedAttributes +{ + for (Class attributeCls in FBElementAttribute.supportedAttributes) { + // include all supported attributes by default unless enumerated explicitly + if (includedAttributes && ![includedAttributes containsObject:attributeCls]) { + continue; + } + // Text-input placeholder (only for elements that support inner text) + if ((attributeCls == FBPlaceholderValueAttribute.class) && + !FBDoesElementSupportInnerText(element.elementType)) { + continue; + } + // Only for elements that support min/max value + if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) && + !FBDoesElementSupportMinMaxValue(element.elementType)) { + continue; + } + int rc = [attributeCls recordWithWriter:writer + forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]]; + if (rc < 0) { + return rc; + } + } + + if (nil != indexPath) { + // index path is the special case + return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath]; + } + if (element.elementType == XCUIElementTypeApplication) { + // only record process identifier and bundle identifier for the application element + int pid = [element.accessibilityElement processIdentifier]; + if (pid > 0) { + int rc = [FBApplicationPidAttribute recordWithWriter:writer + forValue:[NSString stringWithFormat:@"%d", pid]]; + if (rc < 0) { + return rc; + } + XCUIApplication *app = [[FBXCAXClientProxy sharedClient] + monitoredApplicationWithProcessIdentifier:pid]; + NSString *bundleID = [app bundleID]; + if (nil != bundleID) { + rc = [FBApplicationBundleIdAttribute recordWithWriter:writer + forValue:bundleID]; + if (rc < 0) { + return rc; + } + } + } + } + return 0; +} + ++ (int)writeXmlWithRootElement:(id)root + indexPath:(nullable NSString *)indexPath + elementStore:(nullable NSMutableDictionary *)elementStore + includedAttributes:(nullable NSSet *)includedAttributes + writer:(xmlTextWriterPtr)writer +{ + NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil); + + NSArray> *children = root.children; + + if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) { + [elementStore setObject:root forKey:topNodeIndexPath]; + } + + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root]; + int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc]; + return rc; + } + + rc = [self recordElementAttributes:writer + forElement:root + indexPath:indexPath + includedAttributes:includedAttributes]; + if (rc < 0) { + return rc; + } + + for (NSUInteger i = 0; i < [children count]; i++) { + @autoreleasepool { + id childSnapshot = [children objectAtIndex:i]; + NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; + if (elementStore != nil && newIndexPath != nil) { + [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; + } + rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot] + indexPath:newIndexPath + elementStore:elementStore + includedAttributes:includedAttributes + writer:writer]; + if (rc < 0) { + return rc; + } + } + } + + rc = xmlTextWriterEndElement(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc]; + return rc; + } + return 0; +} + ++ (id)snapshotWithRoot:(id)root + useNative:(BOOL)useNative +{ + if (![root isKindOfClass:XCUIElement.class]) { + return (id)root; + } + + if (useNative) { + return [(XCUIElement *)root fb_nativeSnapshot]; + } + return [root isKindOfClass:XCUIApplication.class] + ? [(XCUIElement *)root fb_standardSnapshot] + : [(XCUIElement *)root fb_customSnapshot]; +} + ++ (void)waitUntilStableWithElement:(id)root +{ + if ([root isKindOfClass:XCUIElement.class]) { + // If the app is not idle state while we retrieve the visiblity state + // then the snapshot retrieval operation might freeze and time out + [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + } +} + +@end + + +static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException"; + +@implementation FBElementAttribute + +- (instancetype)initWithElement:(id)element +{ + self = [super init]; + if (self) { + _element = element; + } + return self; +} + ++ (NSString *)name +{ + NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)]; + @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil]; +} + ++ (NSString *)valueForElement:(id)element +{ + NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)]; + @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil]; +} + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element +{ + NSString *value = [self valueForElement:element]; + return [self recordWithWriter:writer forValue:value]; +} + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value +{ + if (nil == value) { + // Skip the attribute if the value equals to nil + return 0; + } + int rc = xmlTextWriterWriteAttribute(writer, + (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String], + (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; + } + return rc; +} + ++ (NSArray *)supportedAttributes +{ + // The list of attributes to be written for each XML node + // The enumeration order does matter here + return @[FBTypeAttribute.class, + FBValueAttribute.class, + FBNameAttribute.class, + FBLabelAttribute.class, + FBEnabledAttribute.class, + FBVisibleAttribute.class, + FBAccessibleAttribute.class, +#if TARGET_OS_TV + FBFocusedAttribute.class, +#endif + FBXAttribute.class, + FBYAttribute.class, + FBWidthAttribute.class, + FBHeightAttribute.class, + FBIndexAttribute.class, + FBHittableAttribute.class, + FBPlaceholderValueAttribute.class, + FBTraitsAttribute.class, + FBNativeFrameAttribute.class, + FBMinValueAttribute.class, + FBMaxValueAttribute.class, + ]; +} + +@end + +@implementation FBTypeAttribute + ++ (NSString *)name +{ + return @"type"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdType; +} + +@end + +@implementation FBValueAttribute + ++ (NSString *)name +{ + return @"value"; +} + ++ (NSString *)valueForElement:(id)element +{ + id idValue = element.wdValue; + if ([idValue isKindOfClass:[NSValue class]]) { + return [idValue stringValue]; + } else if ([idValue isKindOfClass:[NSString class]]) { + return idValue; + } + return [idValue description]; +} + +@end + +@implementation FBNameAttribute + ++ (NSString *)name +{ + return @"name"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdName; +} + +@end + +@implementation FBLabelAttribute + ++ (NSString *)name +{ + return @"label"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdLabel; +} + +@end + +@implementation FBEnabledAttribute + ++ (NSString *)name +{ + return @"enabled"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdEnabled); +} + +@end + +@implementation FBVisibleAttribute + ++ (NSString *)name +{ + return @"visible"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdVisible); +} + +@end + +@implementation FBAccessibleAttribute + ++ (NSString *)name +{ + return @"accessible"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdAccessible); +} + +@end + +#if TARGET_OS_TV + +@implementation FBFocusedAttribute + ++ (NSString *)name +{ + return @"focused"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdFocused); +} + +@end + +#endif + +@implementation FBDimensionAttribute + ++ (NSString *)valueForElement:(id)element +{ + return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]]; +} + +@end + +@implementation FBXAttribute + ++ (NSString *)name +{ + return @"x"; +} + +@end + +@implementation FBYAttribute + ++ (NSString *)name +{ + return @"y"; +} + +@end + +@implementation FBWidthAttribute + ++ (NSString *)name +{ + return @"width"; +} + +@end + +@implementation FBHeightAttribute + ++ (NSString *)name +{ + return @"height"; +} + +@end + +@implementation FBIndexAttribute + ++ (NSString *)name +{ + return @"index"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [NSString stringWithFormat:@"%lu", element.wdIndex]; +} + +@end + +@implementation FBHittableAttribute + ++ (NSString *)name +{ + return @"hittable"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdHittable); +} + +@end + +@implementation FBInternalIndexAttribute + ++ (NSString *)name +{ + return kXMLIndexPathKey; +} + +@end + +@implementation FBApplicationBundleIdAttribute : FBElementAttribute + ++ (NSString *)name +{ + return @"bundleId"; +} + +@end + +@implementation FBApplicationPidAttribute : FBElementAttribute + ++ (NSString *)name +{ + return @"processId"; +} + +@end + +@implementation FBPlaceholderValueAttribute + ++ (NSString *)name +{ + return @"placeholderValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdPlaceholderValue; +} +@end + +@implementation FBNativeFrameAttribute + ++ (NSString *)name +{ + return @"nativeFrame"; +} + ++ (NSString *)valueForElement:(id)element +{ + return NSStringFromCGRect(element.wdNativeFrame); +} +@end + +@implementation FBTraitsAttribute + ++ (NSString *)name +{ + return @"traits"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdTraits; +} + +@end + +@implementation FBMinValueAttribute + ++ (NSString *)name +{ + return @"minValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMinValue stringValue]; +} + +@end + +@implementation FBMaxValueAttribute + ++ (NSString *)name +{ + return @"maxValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMaxValue stringValue]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h new file mode 100644 index 0000000..dbfb4cf --- /dev/null +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface LRUCache : NSObject + +/*! Maximum cache capacity. Could only be set in the constructor */ +@property (nonatomic, readonly) NSUInteger capacity; + +/** + Constructs a new LRU cache instance with the given capacity + + @param capacity Maximum cache capacity + */ +- (instancetype)initWithCapacity:(NSUInteger)capacity; + +/** + Puts a new object into the cache. nil cannot be stored in the cache. + + @param object Object to put + @param key Object's key + */ +- (void)setObject:(id)object forKey:(id)key; + +/** + Retrieves an object from the cache. Every time this method is called the matched + object is bumped in the cache (if exists) + + @param key Object's key + @returns Either the stored instance or nil if the object does not exist or has expired + */ +- (nullable id)objectForKey:(id)key; + +/** + Retrieves all values from the cache ORDERED by recent bump. No bump is performed + + @return Array of all cache values ordred by recent usage (oldest items are at the tail) + */ +- (NSArray *)allObjects; + +/** + Removes the object associated with the specified key from the cache. + + @param key The key identifying the object to remove. + */ +- (void)removeObjectForKey:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m new file mode 100644 index 0000000..731d744 --- /dev/null +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m @@ -0,0 +1,146 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "LRUCache.h" +#import "LRUCacheNode.h" + +@interface LRUCache () +@property (nonatomic) NSMutableDictionary *store; +@property (nonatomic, nullable) LRUCacheNode *headNode; +@property (nonatomic, nullable) LRUCacheNode *tailNode; +@end + +@implementation LRUCache + +- (instancetype)initWithCapacity:(NSUInteger)capacity +{ + if ((self = [super init])) { + _store = [NSMutableDictionary dictionary]; + _capacity = capacity; + } + return self; +} + +- (void)setObject:(id)object forKey:(id)key +{ + NSAssert(nil != object && nil != key, @"LRUCache cannot store nil objects"); + + LRUCacheNode *previousNode = self.store[key]; + if (nil != previousNode) { + [self removeNode:previousNode]; + } + + LRUCacheNode *newNode = [LRUCacheNode nodeWithValue:object key:key]; + self.store[key] = newNode; + [self addNodeToHead:newNode]; + if (nil == previousNode) { + [self alignSize]; + } +} + +- (id)objectForKey:(id)key +{ + LRUCacheNode *node = self.store[key]; + return [self moveNodeToHead:node].value; +} + +- (NSArray *)allObjects +{ + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.store.count]; + LRUCacheNode *node = self.headNode; + while (node != nil) { + [result addObject:node.value]; + node = node.next; + } + return result.copy; +} + +- (nullable LRUCacheNode *)moveNodeToHead:(nullable LRUCacheNode *)node +{ + if (nil == node || node == self.headNode) { + return node; + } + + LRUCacheNode *previousNode = node.prev; + if (nil != previousNode) { + previousNode.next = node.next; + } + LRUCacheNode *nextNode = node.next; + if (nil != nextNode) { + nextNode.prev = node.prev; + } + if (node == self.tailNode) { + self.tailNode = previousNode; + } + return [self addNodeToHead:node]; +} + +- (void)removeNode:(nullable LRUCacheNode *)node +{ + if (nil == node) { + return; + } + + if (nil != node.next) { + node.next.prev = node.prev; + } + if (node == self.headNode) { + self.headNode = node.next; + } + if (nil != node.prev) { + node.prev.next = node.next; + } + if (node == self.tailNode) { + self.tailNode = node.prev; + } + [self.store removeObjectForKey:(id)node.key]; +} + +- (nullable LRUCacheNode *)addNodeToHead:(nullable LRUCacheNode *)node +{ + if (nil == node || node == self.headNode) { + return node; + } + + LRUCacheNode *previousHead = self.headNode; + if (nil != previousHead) { + previousHead.prev = node; + } + node.next = previousHead; + node.prev = nil; + self.headNode = node; + if (nil == self.tailNode) { + self.tailNode = previousHead ?: node; + } + return node; +} + +- (void)alignSize +{ + if (self.store.count > self.capacity && nil != self.tailNode) { + [self removeNode:self.tailNode]; + } +} + +- (void)removeObjectForKey:(id)key +{ + LRUCacheNode *node = self.store[key]; + if (node != nil) { + [self removeNode:node]; + } +} + +@end diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h b/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h new file mode 100644 index 0000000..adde370 --- /dev/null +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface LRUCacheNode : NSObject + +/*! Node value */ +@property (nonatomic, readonly) id value; +/*! Node key */ +@property (nonatomic, readonly) id key; +/*! Pointer to the next node */ +@property (nonatomic, nullable) LRUCacheNode *next; +/*! Pointer to the previous node */ +@property (nonatomic, nullable) LRUCacheNode *prev; + +/** + Factory method to create a new cache node with the given value and key + + @param value Node value + @param key Node key + @returns Cache node instance + */ ++ (instancetype)nodeWithValue:(id)value key:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m b/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m new file mode 100644 index 0000000..b3bcb14 --- /dev/null +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "LRUCacheNode.h" + +@interface LRUCacheNode () +@property (nonatomic, readwrite) id value; +@property (nonatomic, readwrite) id key; + +- (instancetype)initWithValue:(id)value key:(id)key; +@end + +@implementation LRUCacheNode + +- (instancetype)initWithValue:(id)value key:(id)key +{ + if (nil == value) { + return nil; + } + + if ((self = [super init])) { + _value = value; + _key = key; + } + return self; +} + ++ (instancetype)nodeWithValue:(id)value key:(id)key +{ + return [[LRUCacheNode alloc] initWithValue:value key:key]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ %@", self.value, self.next]; +} + +@end diff --git a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h new file mode 100644 index 0000000..a7ad285 --- /dev/null +++ b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSPredicate (FBFormat) + +/** + Method used to normalize/verify NSPredicate expressions before passing them to WDA. + Only expressions of NSKeyPathExpressionType are going to be verified. + Allowed property names are only these declared in FBElement protocol (property names are received in runtime) + and their shortcuts (without 'wd' prefix). All other property names are considered as unknown. + + @param input predicate object received from user input + @return formatted predicate + @throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol + */ ++ (instancetype)fb_formatSearchPredicate:(NSPredicate *)input; + +/** + Creates a block predicate expression, which properly evalluates the given raw predicate agains + xctest hierarchy. Vanilla string predicates don't work on this hierachy because "raw" snapshots + don't have any of the custom properties declared in FBElement protocol. + `fb_formatSearchPredicate` is called automtically on the original predicate before + making it to a block. + + @param input predicate object received from user input + @return formatted predicate + @throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol + */ ++ (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m new file mode 100644 index 0000000..65af951 --- /dev/null +++ b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSPredicate+FBFormat.h" + +#import "NSExpression+FBFormat.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" + +@implementation NSPredicate (FBFormat) + ++ (instancetype)fb_predicateWithPredicate:(NSPredicate *)original + comparisonModifier:(NSPredicate *(^)(NSComparisonPredicate *))comparisonModifier +{ + if ([original isKindOfClass:NSCompoundPredicate.class]) { + NSCompoundPredicate *compPred = (NSCompoundPredicate *)original; + NSMutableArray *predicates = [NSMutableArray array]; + for (NSPredicate *predicate in [compPred subpredicates]) { + NSPredicate *newPredicate = [self.class fb_predicateWithPredicate:predicate + comparisonModifier:comparisonModifier]; + if (nil != newPredicate) { + [predicates addObject:newPredicate]; + } + } + return [[NSCompoundPredicate alloc] initWithType:compPred.compoundPredicateType + subpredicates:predicates]; + } + if ([original isKindOfClass:NSComparisonPredicate.class]) { + return comparisonModifier((NSComparisonPredicate *)original); + } + return original; +} + ++ (instancetype)fb_formatSearchPredicate:(NSPredicate *)input +{ + return [self.class fb_predicateWithPredicate:input + comparisonModifier:^NSPredicate *(NSComparisonPredicate *cp) { + NSExpression *left = [NSExpression fb_wdExpressionWithExpression:[cp leftExpression]]; + NSExpression *right = [NSExpression fb_wdExpressionWithExpression:[cp rightExpression]]; + return [NSComparisonPredicate predicateWithLeftExpression:left + rightExpression:right + modifier:cp.comparisonPredicateModifier + type:cp.predicateOperatorType + options:cp.options]; + }]; +} + ++ (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input +{ + if ([NSStringFromClass(input.class) isEqualToString:@"NSBlockPredicate"]) { + return input; + } + + NSPredicate *wdPredicate = [self.class fb_formatSearchPredicate:input]; + return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, + NSDictionary * _Nullable bindings) { + @autoreleasepool { + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject]; + return [wdPredicate evaluateWithObject:wrappedSnapshot]; + } + }]; +} + + +@end diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h new file mode 100644 index 0000000..853ba41 --- /dev/null +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol XCDebugLogDelegate; + +/*! Accessibility identifier for is visible attribute */ +extern NSNumber *FB_XCAXAIsVisibleAttribute; +extern NSString *FB_XCAXAIsVisibleAttributeName; + +/*! Accessibility identifier for is accessible attribute */ +extern NSNumber *FB_XCAXAIsElementAttribute; +extern NSString *FB_XCAXAIsElementAttributeName; + +/*! Accessibility identifier for visible frame attribute */ +extern NSString *FB_XCAXAVisibleFrameAttributeName; + +/*! Accessibility identifier для минимума */ +extern NSNumber *FB_XCAXACustomMinValueAttribute; +extern NSString *FB_XCAXACustomMinValueAttributeName; + +/*! Accessibility identifier для максимума */ +extern NSNumber *FB_XCAXACustomMaxValueAttribute; +extern NSString *FB_XCAXACustomMaxValueAttributeName; + +/*! Getter for XCTest logger */ +extern id (*XCDebugLogger)(void); + +/*! Setter for XCTest logger */ +extern void (*XCSetDebugLogger)(id ); + +/*! Maps string attributes to AX Accesibility Attributes*/ +extern NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id stringAttributes); + +/** + Method used to retrieve pointer for given symbol 'name' from given 'binary' + + @param name name of the symbol + @return pointer to symbol + */ +void *FBRetrieveXCTestSymbol(const char *name); + +/*! Static constructor that will retrieve XCTest private symbols */ +__attribute__((constructor)) void FBLoadXCTestSymbols(void); + +/** + Method is used to tranform attribute names into the format, which + is acceptable for the internal XCTest snpshoting API + + @param attributeNames set of attribute names. Must be on of FB_..Name constants above + @returns The array of tranformed values. Unknown values are silently skipped + */ +NSArray *FBCreateAXAttributes(NSSet *attributeNames); + +/** + Retrives the set of standard attribute names + + @returns Array of FB_..Name constants above, which represent standard element attributes + */ +NSArray *FBStandardAttributeNames(void); + +/** +Retrives the set of custom attribute names. These attributes are normally not accessible + by public XCTest calls, but are still available in the accessibility framework + +@returns Array of FB_..Name constants above, which represent custom element attributes +*/ +NSArray *FBCustomAttributeNames(void); diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m new file mode 100644 index 0000000..244e3fd --- /dev/null +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCTestPrivateSymbols.h" + +#import + +#import "FBRuntimeUtils.h" +#import "FBXCodeCompatibility.h" + +NSNumber *FB_XCAXAIsVisibleAttribute; +NSString *FB_XCAXAIsVisibleAttributeName = @"XC_kAXXCAttributeIsVisible"; +NSNumber *FB_XCAXAIsElementAttribute; +NSString *FB_XCAXAIsElementAttributeName = @"XC_kAXXCAttributeIsElement"; +NSString *FB_XCAXAVisibleFrameAttributeName = @"XC_kAXXCAttributeVisibleFrame"; +NSNumber *FB_XCAXACustomMinValueAttribute; +NSString *FB_XCAXACustomMinValueAttributeName = @"XC_kAXXCAttributeMinValue"; +NSNumber *FB_XCAXACustomMaxValueAttribute; +NSString *FB_XCAXACustomMaxValueAttributeName = @"XC_kAXXCAttributeMaxValue"; + +void (*XCSetDebugLogger)(id ); +id (*XCDebugLogger)(void); + +NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id); + +__attribute__((constructor)) void FBLoadXCTestSymbols(void) +{ + NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]); + NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]); + + XCAXAccessibilityAttributesForStringAttributes = + (NSArray *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes"); + + XCSetDebugLogger = (void (*)(id ))FBRetrieveXCTestSymbol("XCSetDebugLogger"); + XCDebugLogger = (id(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger"); + + NSArray *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]); + FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0]; + FB_XCAXAIsElementAttribute = accessibilityAttributes[1]; + + NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); + NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); + + NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]); + NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]); + + NSArray *minMaxAttrs = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeMinValue, XC_kAXXCAttributeMaxValue]); + FB_XCAXACustomMinValueAttribute = minMaxAttrs[0]; + FB_XCAXACustomMaxValueAttribute = minMaxAttrs[1]; + + NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute); + NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute); +} + +void *FBRetrieveXCTestSymbol(const char *name) +{ + Class XCTestClass = objc_lookUpClass("XCTestCase"); + NSCAssert(XCTestClass != nil, @"XCTest should be already linked", XCTestClass); + NSString *XCTestBinary = [NSBundle bundleForClass:XCTestClass].executablePath; + const char *binaryPath = XCTestBinary.UTF8String; + NSCAssert(binaryPath != nil, @"XCTest binary path should not be nil", binaryPath); + return FBRetrieveSymbolFromBinary(binaryPath, name); +} + +NSArray *FBStandardAttributeNames(void) +{ + static NSArray *attributeNames; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class xcElementSnapshotClass = NSClassFromString(@"XCElementSnapshot"); + NSCAssert(nil != xcElementSnapshotClass, @"XCElementSnapshot class must be resolvable", xcElementSnapshotClass); + attributeNames = [xcElementSnapshotClass sanitizedElementSnapshotHierarchyAttributesForAttributes:nil + isMacOS:NO]; + }); + return attributeNames; +} + +NSArray *FBCustomAttributeNames(void) +{ + static NSArray *customNames; + static dispatch_once_t onceCustomAttributeNamesToken; + dispatch_once(&onceCustomAttributeNamesToken, ^{ + customNames = @[ + FB_XCAXAIsVisibleAttributeName, + FB_XCAXAIsElementAttributeName, + FB_XCAXACustomMinValueAttributeName, + FB_XCAXACustomMaxValueAttributeName + ]; + }); + return customNames; +} diff --git a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h new file mode 100644 index 0000000..3cbb5ef --- /dev/null +++ b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + In certain cases WebDriverAgent fails to create a session because -[XCUIApplication launch] doesn't return + since it waits for the target app to be quiescenced. + The reason for this seems to be that 'testmanagerd' doesn't send the events WebDriverAgent is waiting for. + The expected events would trigger calls to '-[XCUIApplicationProcess setEventLoopHasIdled:]' and + '-[XCUIApplicationProcess setAnimationsHaveFinished:]', which are the properties that are checked to + determine whether an app has quiescenced or not. + Delaying the call to on of the setters can fix this issue. + */ +@interface XCUIApplicationProcessDelay : NSObject + +/** + Delays the invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]' by the timer interval passed + @param delay The duration of the sleep before the original method is called + */ ++ (void)setEventLoopHasIdledDelay:(NSTimeInterval)delay; + +/** + Disables the delayed invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]'. + */ ++ (void)disableEventLoopDelay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m new file mode 100644 index 0000000..e41a0a7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplicationProcessDelay.h" +#import +#import "XCUIApplicationProcess.h" +#import "FBLogger.h" + +static void (*orig_set_event_loop_has_idled)(id, SEL, BOOL); +static NSTimeInterval eventloopIdleDelay = 0; +static BOOL isSwizzled = NO; +// '-[XCUIApplicationProcess setEventLoopHasIdled:]' can be called from different queues. +// Lets lock the setup and access to the 'eventloopIdleDelay' variable +static NSLock * lock = nil; + +@implementation XCUIApplicationProcessDelay + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + ++ (void)load +{ + lock = [[NSLock alloc] init]; +} + +#pragma clang diagnostic pop + ++ (void)setEventLoopHasIdledDelay:(NSTimeInterval)delay +{ + [lock lock]; + if (!isSwizzled && delay < DBL_EPSILON) { + // don't swizzle methods until we need to + [lock unlock]; + return; + } + eventloopIdleDelay = delay; + if (isSwizzled) { + [lock unlock]; + return; + } + [self swizzleSetEventLoopHasIdled]; + [lock unlock]; +} + ++ (void)disableEventLoopDelay +{ + // Once the methods were swizzled they stay like that since the only change in the implementation + // is the thread sleep, which is skipped on setting it to zero. + [self setEventLoopHasIdledDelay:0]; +} + ++ (void)swizzleSetEventLoopHasIdled { + Method original = class_getInstanceMethod([XCUIApplicationProcess class], @selector(setEventLoopHasIdled:)); + if (original == nil) { + [FBLogger log:@"Could not find method -[XCUIApplicationProcess setEventLoopHasIdled:]"]; + return; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + orig_set_event_loop_has_idled = (void(*)(id, SEL, BOOL)) method_getImplementation(original); +#pragma clang diagnostic pop + Method replace = class_getClassMethod([XCUIApplicationProcessDelay class], @selector(setEventLoopHasIdled:)); + method_setImplementation(original, method_getImplementation(replace)); + isSwizzled = YES; +} + ++ (void)setEventLoopHasIdled:(BOOL)idled { + [lock lock]; + NSTimeInterval delay = eventloopIdleDelay; + [lock unlock]; + if (delay > 0.0) { + [FBLogger verboseLog:[NSString stringWithFormat:@"Delaying -[XCUIApplicationProcess setEventLoopHasIdled:] by %.2f seconds", delay]]; + [NSThread sleepForTimeInterval:delay]; + } + orig_set_event_loop_has_idled(self, _cmd, idled); +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h new file mode 100644 index 0000000..92d53a4 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h @@ -0,0 +1,1220 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; +@protocol GCDAsyncSocketDelegate; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +extern NSString *const GCDAsyncSocketSSLALPN; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ERROR_ENUM(GCDAsyncSocketErrorDomain, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +/** + * Create GCDAsyncSocket from already connect BSD socket file descriptor +**/ ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError **)error; + +#pragma mark Configuration + +@property (atomic, weak, readwrite, nullable) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 + * this is the delay between connecting to the preferred protocol and the fallback protocol. + * + * Defaults to 300ms. +**/ +@property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite, nullable) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can be used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly, nullable) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly, nullable) NSURL *connectedUrl; + +@property (atomic, readonly, nullable) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly, nullable) NSData *connectedAddress; +@property (atomic, readonly, nullable) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating an SSLCipherSuite. + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(nullable NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (nullable SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m new file mode 100755 index 0000000..de18142 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -0,0 +1,8528 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +NSString *const GCDAsyncSocketSSLALPN = @"GCDAsyncSocketSSLALPN"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams + + + static uint64_t cfstreamThreadRetainCount; // setup & teardown + static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (instancetype)initWithCapacity:(size_t)numBytes NS_DESIGNATED_INITIALIZER; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +// Cover the superclass' designated initializer +- (instancetype)init NS_UNAVAILABLE +{ + NSAssert(0, @"Use the designated initializer"); + return nil; +} + +- (instancetype)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (instancetype)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i NS_DESIGNATED_INITIALIZER; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +// Cover the superclass' designated initializer +- (instancetype)init NS_UNAVAILABLE +{ + NSAssert(0, @"Use the designated initializer"); + return nil; +} + +- (instancetype)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + result = readLength - bytesDone; + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvla" + uint8_t seq[termLength]; +#pragma clang diagnostic pop + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; +@end + +@implementation GCDAsyncWritePacket + +// Cover the superclass' designated initializer +- (instancetype)init NS_UNAVAILABLE +{ + NSAssert(0, @"Use the designated initializer"); + return nil; +} + +- (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (instancetype)initWithTLSSettings:(NSDictionary *)settings NS_DESIGNATED_INITIALIZER; +@end + +@implementation GCDAsyncSpecialPacket + +// Cover the superclass' designated initializer +- (instancetype)init NS_UNAVAILABLE +{ + NSAssert(0, @"Use the designated initializer"); + return nil; +} + +- (instancetype)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int socketUN; + NSURL *socketUrl; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + NSData * connectInterfaceUN; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t acceptUNSource; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + OSStatus lastSSLHandshakeError; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; + NSTimeInterval alternateAddressDelay; +} + +- (instancetype)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (instancetype)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + socketUrl = nil; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + alternateAddressDelay = 0.3; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +#pragma mark - + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error { + return [self socketFromConnectedSocketFD:socketFD delegate:nil delegateQueue:NULL socketQueue:sq error:error]; +} + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error { + return [self socketFromConnectedSocketFD:socketFD delegate:aDelegate delegateQueue:dq socketQueue:NULL error:error]; +} + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError* __autoreleasing *)error +{ + GCDAsyncSocket *socket = [[[self class] alloc] initWithDelegate:aDelegate delegateQueue:dq socketQueue:sq]; + + __block NSError *innerError = nil; + dispatch_sync(socket->socketQueue, ^{ @autoreleasepool { + struct sockaddr addr; + socklen_t addr_size = sizeof(struct sockaddr); + int retVal = getpeername(socketFD, (struct sockaddr *)&addr, &addr_size); + if (retVal) + { + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to create socket from socket FD failed. getpeername() failed", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketOtherError + userInfo:userInfo]; + return; + } + + if (addr.sa_family == AF_INET) + { + socket->socket4FD = socketFD; + } + else if (addr.sa_family == AF_INET6) + { + socket->socket6FD = socketFD; + } + else + { + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketOtherError + userInfo:userInfo]; + return; + } + + socket->flags = kSocketStarted; + [socket didConnect:socket->stateIndex]; + }}); + + if (nil != innerError && nil != error) { + *error = innerError; + } + return nil == innerError ? socket : nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = self->delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + self->delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = self->delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = self->delegate; + dqPtr = self->delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + self->delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kIPv4Disabled; + else + self->config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kIPv6Disabled; + else + self->config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kPreferIPv6; + else + self->config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (NSTimeInterval) alternateAddressDelay { + __block NSTimeInterval delay; + dispatch_block_t block = ^{ + delay = self->alternateAddressDelay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + return delay; +} + +- (void) setAlternateAddressDelay:(NSTimeInterval)delay { + dispatch_block_t block = ^{ + self->alternateAddressDelay = delay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = self->userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (self->userData != arbitraryUserData) + { + self->userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errorWithErrno:errno reason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (self->delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [self->readQueue removeAllObjects]; + [self->writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + self->socket4FD = createSocket(AF_INET, interface4); + + if (self->socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + self->socket6FD = createSocket(AF_INET6, interface6); + + if (self->socket6FD == SOCKET_NULL) + { + if (self->socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(self->socket4FD); + self->socket4FD = SOCKET_NULL; + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + self->accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket4FD, 0, self->socketQueue); + + int socketFD = self->socket4FD; + dispatch_source_t acceptSource = self->accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(self->accept4Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(self->accept4Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(self->accept4Source); + } + + if (enableIPv6) + { + self->accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket6FD, 0, self->socketQueue); + + int socketFD = self->socket6FD; + dispatch_source_t acceptSource = self->accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(self->accept6Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(self->accept6Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(self->accept6Source); + } + + self->flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errorWithErrno:errno reason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (self->delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [self->readQueue removeAllObjects]; + [self->writeQueue removeAllObjects]; + + // Remove a previous socket + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *urlPath = url.path; + if (urlPath && [fileManager fileExistsAtPath:urlPath]) { + if (![fileManager removeItemAtURL:url error:&error]) { + NSString *msg = @"Could not remove previous unix domain socket at given url."; + err = [self otherError:msg]; + + return_from_block; + } + } + + // Resolve interface from description + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create sockets, configure, bind, and listen + + LogVerbose(@"Creating unix domain socket"); + self->socketUN = createSocket(AF_UNIX, interface); + + if (self->socketUN == SOCKET_NULL) + { + return_from_block; + } + + self->socketUrl = url; + + // Create accept sources + + self->acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socketUN, 0, self->socketQueue); + + int socketFD = self->socketUN; + dispatch_source_t acceptSource = self->acceptUNSource; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(self->acceptUNSource, ^{ @autoreleasepool { + + __strong GCDAsyncSocket *strongSelf = weakSelf; + + LogVerbose(@"eventUNBlock"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(self->acceptUNSource, ^{ + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(acceptUNSource)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socketUN)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(acceptUNSource)"); + dispatch_resume(self->acceptUNSource); + + self->flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + int socketType; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + socketType = 0; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else if (parentSocketFD == socket6FD) + { + socketType = 1; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socketUN) + { + socketType = 2; + + struct sockaddr_un addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + LogVerbose(@"close(childSocketFD)"); + close(childSocketFD); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate + delegateQueue:self->delegateQueue + socketQueue:childSocketQueue]; + + if (socketType == 0) + acceptedSocket->socket4FD = childSocketFD; + else if (socketType == 1) + acceptedSocket->socket6FD = childSocketFD; + else + acceptedSocket->socketUN = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterfaceUN = interface; + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + self->flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = self->stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [[self class] isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [[self class] isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + + #pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + self->flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([url.path length] == 0) + { + NSString *msg = @"Invalid unix domain socket url."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithUrl:url error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + self->flags |= kSocketStarted; + + // Start the normal connection process + + NSError *connectError = nil; + if (![self connectWithAddressUN:self->connectInterfaceUN error:&connectError]) + { + [self closeWithError:connectError]; + + return_from_block; + } + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr +{ + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in bind() function"]; + + return NO; + } + } + + return YES; +} + +- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr +{ + int socketFD = socket(family, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; + + return socketFD; + } + + if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) + { + [self closeSocket:socketFD]; + + return SOCKET_NULL; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + return socketFD; +} + +- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex +{ + // If there already is a socket connected, we close socketFD and return + if (self.isConnected) + { + [self closeSocket:socketFD]; + return; + } + + // Start the connection process in a background queue + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + int err = errno; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + if (strongSelf.isConnected) + { + [strongSelf closeSocket:socketFD]; + return_from_block; + } + + if (result == 0) + { + [self closeUnusedSocket:socketFD]; + + [strongSelf didConnect:aStateIndex]; + } + else + { + [strongSelf closeSocket:socketFD]; + + // If there are no more sockets trying to connect, we inform the error to the delegate + if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) + { + NSError *error = [strongSelf errorWithErrno:err reason:@"Error in connect() function"]; + [strongSelf didNotConnect:aStateIndex error:error]; + } + } + }}); + +#pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); +} + +- (void)closeSocket:(int)socketFD +{ + if (socketFD != SOCKET_NULL && + (socketFD == socket6FD || socketFD == socket4FD)) + { + close(socketFD); + + if (socketFD == socket4FD) + { + LogVerbose(@"close(socket4FD)"); + socket4FD = SOCKET_NULL; + } + else if (socketFD == socket6FD) + { + LogVerbose(@"close(socket6FD)"); + socket6FD = SOCKET_NULL; + } + } +} + +- (void)closeUnusedSocket:(int)usedSocketFD +{ + if (usedSocketFD != socket4FD) + { + [self closeSocket:socket4FD]; + } + else if (usedSocketFD != socket6FD) + { + [self closeSocket:socket6FD]; + } +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + // Create and bind the sockets + + if (address4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; + } + + if (address6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + return NO; + } + + int socketFD, alternateSocketFD; + NSData *address, *alternateAddress; + + if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) + { + socketFD = socket6FD; + alternateSocketFD = socket4FD; + address = address6; + alternateAddress = address4; + } + else + { + socketFD = socket4FD; + alternateSocketFD = socket6FD; + address = address4; + alternateAddress = address6; + } + + int aStateIndex = stateIndex; + + [self connectSocket:socketFD address:address stateIndex:aStateIndex]; + + if (alternateAddress) + { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ + [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; + }); + } + + return YES; +} + +- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // Create the socket + + int socketFD; + + LogVerbose(@"Creating unix domain socket"); + + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); + + socketFD = socketUN; + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + LogVerbose(@"Binding socket..."); + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + +// const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; +// +// int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); +// if (result != 0) +// { +// if (errPtr) +// *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; +// +// return NO; +// } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; + int result = connect(socketFD, addr, addr->sa_len); + if (result == 0) + { + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self didConnect:aStateIndex]; + }}); + } + else + { + // TODO: Bad file descriptor + perror("connect"); + NSError *error = [self errorWithErrno:errno reason:@"Error in connect() function"]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aStateIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aStateIndex != self->stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + NSURL *url = [self connectedUrl]; + + __strong id theDelegate = delegate; + + if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToUrl:url]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + + [sslPreBuffer reset]; + sslErrCode = lastSSLHandshakeError = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + + if (socketUN != SOCKET_NULL) + { + LogVerbose(@"close(socketUN)"); + close(socketUN); + socketUN = SOCKET_NULL; + unlink(socketUrl.path.fileSystemRepresentation); + socketUrl = nil; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (acceptUNSource) + { + LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); + dispatch_source_cancel(acceptUNSource); + + // We never suspend acceptUNSource + + acceptUNSource = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + sslWriteCachedLength = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errorWithErrno:(int)err reason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(err)]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg, + NSLocalizedFailureReasonErrorKey : reason}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = @{NSLocalizedRecoverySuggestionErrorKey : msg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:self->socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (self->socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:self->socket6FD]; + }); + + return result; + } +} + +- (NSURL *)connectedUrl +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socketUN != SOCKET_NULL) + return [self connectedUrlFromSocketUN:socketUN]; + + return nil; + } + else + { + __block NSURL *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socketUN != SOCKET_NULL) + result = [self connectedUrlFromSocketUN:self->socketUN]; + }}); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:self->socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (self->socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:self->socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSURL *)connectedUrlFromSocketUN:(int)socketFD +{ + struct sockaddr_un sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) + { + return 0; + } + return [[self class] urlFromSockaddrUN:&sockaddr]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (self->socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (self->socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (self->socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (self->socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (self->socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (self->socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (self->flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + NSString *temp = [components objectAtIndex:1]; + long portL = strtol([temp UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url +{ + NSString *path = url.path; + if (path.length == 0) { + return nil; + } + + struct sockaddr_un nativeAddr; + nativeAddr.sun_family = AF_UNIX; + strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); + nativeAddr.sun_len = (unsigned char)SUN_LEN(&nativeAddr); + NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; + + return interface; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + + #pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!self->currentRead || ![self->currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = self->currentRead->bytesDone; + NSUInteger total = self->currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = self->currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + + #if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + + #endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = self->socketFDBytesAvailable + [self->sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(self->sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occurred + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errorWithErrno:errno reason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + // + // We ensure that `waiting` is set in order to resume the readSource (if it is suspended). It is + // possible to reach this point and `waiting` not be set, if the current read's length is + // sufficiently large. In that case, we may have read to some upperbound successfully, but + // that upperbound could be smaller than the desired length. + waiting = YES; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!self->currentWrite || ![self->currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = self->currentWrite->bytesDone; + NSUInteger total = [self->currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = self->currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errorWithErrno:errno reason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errorWithErrno:errno reason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((self->flags & kSocketStarted) && !(self->flags & kQueuedTLS) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self->writeQueue addObject:packet]; + + self->flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = @{}; + if (tlsPacket) { + tlsSettings = tlsPacket->tlsSettings; + } + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue]) + useSecureTransport = NO; + } + #endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + NSNumber *isServerNumber = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer]; + BOOL isServer = [isServerNumber boolValue]; + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } + #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + NSNumber *shouldManuallyEvaluateTrust = [tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust]; + if ([shouldManuallyEvaluateTrust boolValue]) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // 10. GCDAsyncSocketSSLALPN + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + NSObject *value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *certs = (NSArray *)value; + + status = SSLSetCertificate(sslContext, (__bridge CFArrayRef)certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + NSNumber *falseStart = (NSNumber *)value; + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [falseStart boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + NSNumber *oneByteRecord = (NSNumber *)value; + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [oneByteRecord boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvla" + SSLCipherSuite ciphers[numberCiphers]; +#pragma clang diagnostic pop + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = (SSLCipherSuite)[cipherObject unsignedIntValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } + #endif + + // 10. kCFStreamSSLCertificates + value = [tlsSettings objectForKey:GCDAsyncSocketSSLALPN]; + if ([value isKindOfClass:[NSArray class]]) + { + if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)) + { + CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); + status = SSLSetALPNProtocols(sslContext, protocols); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; + return; + } + } + else + { + NSAssert(NO, @"Security option unavailable - GCDAsyncSocketSSLALPN" + @" - iOS 11.0, macOS 10.13 required"); + [self closeWithError:[self otherError:@"Security option unavailable - GCDAsyncSocketSSLALPN"]]; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLALPN. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLALPN."]]; + return; + } + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = lastSSLHandshakeError = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + lastSSLHandshakeError = status; + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + + #pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread:) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [[self class] performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + + #pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread:(id)unused { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-enum" + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } +#pragma clang diagnostic pop +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-enum" + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } +#pragma clang diagnostic pop +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + dispatch_sync(cfstreamThreadSetupQueue, ^{ + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + }); + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + }); + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kAllowHalfDuplexConnection; + else + self->config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occurred creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +#pragma clang diagnostic pop + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Fixes connection issues with IPv6 + // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 + + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; + in_port_t *portPtr = &sockaddr->sin6_port; + if ((portPtr != NULL) && (*portPtr == 0)) { + *portPtr = htons(port); + } + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr +{ + NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; + return [NSURL fileURLWithPath:path]; +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end + +#pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h new file mode 100644 index 0000000..af327e0 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h @@ -0,0 +1,1036 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN +extern NSString *const GCDAsyncUdpSocketException; +extern NSString *const GCDAsyncUdpSocketErrorDomain; + +extern NSString *const GCDAsyncUdpSocketQueueName; +extern NSString *const GCDAsyncUdpSocketThreadName; + +typedef NS_ERROR_ENUM(GCDAsyncUdpSocketErrorDomain, GCDAsyncUdpSocketError) { + GCDAsyncUdpSocketNoError = 0, // Never used + GCDAsyncUdpSocketBadConfigError, // Invalid configuration + GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed + GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out + GCDAsyncUdpSocketClosedError, // The socket was closed + GCDAsyncUdpSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@class GCDAsyncUdpSocket; + +@protocol GCDAsyncUdpSocketDelegate +@optional + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection is successful. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address; + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection fails. + * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError * _Nullable)error; + +/** + * Called when the datagram with the given tag has been sent. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag; + +/** + * Called if an error occurs while trying to send a datagram. + * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError * _Nullable)error; + +/** + * Called when the socket has received the requested datagram. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data + fromAddress:(NSData *)address + withFilterContext:(nullable id)filterContext; + +/** + * Called when the socket is closed. +**/ +- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError * _Nullable)error; + +@end + +/** + * You may optionally set a receive filter for the socket. + * A filter can provide several useful features: + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param context - Out parameter you may optionally set, which will then be passed to the delegate method. + * For example, filter block can parse the data and then, + * pass the parsed data to the delegate. + * + * @returns - YES if the received packet should be passed onto the delegate. + * NO if the received packet should be discarded, and not reported to the delegete. + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * +**/ +typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id __nullable * __nonnull context); + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param tag - The tag that was passed in the send method. + * + * @returns - YES if the packet should actually be sent over the socket. + * NO if the packet should be silently dropped (not sent over the socket). + * + * Regardless of the return value, the delegate will be informed that the packet was successfully sent. + * +**/ +typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag); + + +@interface GCDAsyncUdpSocket : NSObject + +/** + * GCDAsyncUdpSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +#pragma mark Configuration + +- (nullable id)delegate; +- (void)setDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegate:(nullable id)delegate; + +- (nullable dispatch_queue_t)delegateQueue; +- (void)setDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * This means GCDAsyncUdpSocket automatically supports both protocols, + * and can send to IPv4 or IPv6 addresses, + * as well as receive over IPv4 and IPv6. + * + * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6. + * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference. + * If IPv4 is preferred, then IPv4 is used. + * If IPv6 is preferred, then IPv6 is used. + * If neutral, then the first IP version in the resolved array will be used. + * + * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral. + * On prior systems the default IP preference is IPv4. + **/ +- (BOOL)isIPv4Enabled; +- (void)setIPv4Enabled:(BOOL)flag; + +- (BOOL)isIPv6Enabled; +- (void)setIPv6Enabled:(BOOL)flag; + +- (BOOL)isIPv4Preferred; +- (BOOL)isIPv6Preferred; +- (BOOL)isIPVersionNeutral; + +- (void)setPreferIPv4; +- (void)setPreferIPv6; +- (void)setIPVersionNeutral; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. + * The default maximum size is 65535 bytes. + * + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * Since the OS/GCD notifies us of the size of each received UDP packet, + * the actual allocated buffer size for each packet is exact. + * And in practice the size of UDP packets is generally much smaller than the max. + * Indeed most protocols will send and receive packets of only a few bytes, + * or will set a limit on the size of packets to prevent fragmentation in the IP layer. + * + * If you set the buffer size too small, the sockets API in the OS will silently discard + * any extra data, and you will not be notified of the error. +**/ +- (uint16_t)maxReceiveIPv4BufferSize; +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max; + +- (uint32_t)maxReceiveIPv6BufferSize; +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for send operations. + * The default maximum size is 65535 bytes. + * + * Given that a typical link MTU is 1500 bytes, a large UDP datagram will have to be + * fragmented, and that’s both expensive and risky (if one fragment goes missing, the + * entire datagram is lost). You are much better off sending a large number of smaller + * UDP datagrams, preferably using a path MTU algorithm to avoid fragmentation. + * + * You must set it before the sockt is created otherwise it won't work. + * + **/ +- (uint16_t)maxSendBufferSize; +- (void)setMaxSendBufferSize:(uint16_t)max; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally in any way. +**/ +- (nullable id)userData; +- (void)setUserData:(nullable id)arbitraryUserData; + +#pragma mark Diagnostics + +/** + * Returns the local address info for the socket. + * + * The localAddress method returns a sockaddr structure wrapped in a NSData object. + * The localHost method returns the human readable IP address as a string. + * + * Note: Address info may not be available until after the socket has been binded, connected + * or until after data has been sent. +**/ +- (nullable NSData *)localAddress; +- (nullable NSString *)localHost; +- (uint16_t)localPort; + +- (nullable NSData *)localAddress_IPv4; +- (nullable NSString *)localHost_IPv4; +- (uint16_t)localPort_IPv4; + +- (nullable NSData *)localAddress_IPv6; +- (nullable NSString *)localHost_IPv6; +- (uint16_t)localPort_IPv6; + +/** + * Returns the remote address info for the socket. + * + * The connectedAddress method returns a sockaddr structure wrapped in a NSData object. + * The connectedHost method returns the human readable IP address as a string. + * + * Note: Since UDP is connectionless by design, connected address info + * will not be available unless the socket is explicitly connected to a remote host/port. + * If the socket is not connected, these methods will return nil / 0. +**/ +- (nullable NSData *)connectedAddress; +- (nullable NSString *)connectedHost; +- (uint16_t)connectedPort; + +/** + * Returns whether or not this socket has been connected to a single host. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * If connected, the socket will only be able to send/receive data to/from the connected host. +**/ +- (BOOL)isConnected; + +/** + * Returns whether or not this socket has been closed. + * The only way a socket can be closed is if you explicitly call one of the close methods. +**/ +- (BOOL)isClosed; + +/** + * Returns whether or not this socket is IPv4. + * + * By default this will be true, unless: + * - IPv4 is disabled (via setIPv4Enabled:) + * - The socket is explicitly bound to an IPv6 address + * - The socket is connected to an IPv6 address +**/ +- (BOOL)isIPv4; + +/** + * Returns whether or not this socket is IPv6. + * + * By default this will be true, unless: + * - IPv6 is disabled (via setIPv6Enabled:) + * - The socket is explicitly bound to an IPv4 address + * _ The socket is connected to an IPv4 address + * + * This method will also return false on platforms that do not support IPv6. + * Note: The iPhone does not currently support IPv6. +**/ +- (BOOL)isIPv6; + +#pragma mark Binding + +/** + * Binds the UDP socket to the given port. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given port and optional interface. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept packets from the local machine. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port interface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects the UDP socket to the given host and port. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific host/port has the following effect: + * - You will only be able to send data to the connected host/port. + * - You will only be able to receive data from the connected host/port. + * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". + * + * The actual process of connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after it has been connected. + * You can only connect a socket once. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * + * This method is asynchronous as it requires a DNS lookup to resolve the given host name. + * If an obvious error is detected, this method immediately returns NO and sets errPtr. + * If you don't care about the error, you can pass nil for errPtr. + * Otherwise, this method returns YES and begins the asynchronous connection process. + * The result of the asynchronous connection process will be reported via the delegate methods. + **/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific address has the following effect: + * - You will only be able to send data to the connected address. + * - You will only be able to receive data from the connected address. + * - You will receive ICMP messages that come from the connected address, such as "connection refused". + * + * Connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after its been connected. + * You can only connect a socket once. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. + * + * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup. + * Thus when this method returns, the connection has either failed or fully completed. + * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method. + * However, for compatibility and simplification of delegate code, if this method returns YES + * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +#pragma mark Multicast + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr; +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Send multicast on a specified interface. + * For IPv4, interface should be the the IP address of the interface (eg @"192.168.10.1"). + * For IPv6, interface should be the a network interface name (eg @"en0"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ + +- (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; +- (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; + +#pragma mark Reuse Port + +/** + * By default, only one socket can be bound to a given IP address + port at a time. + * To enable multiple processes to simultaneously bind to the same address+port, + * you need to enable this functionality in the socket. All processes that wish to + * use the address+port simultaneously must all enable reuse port on the socket + * bound to that port. + **/ +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Broadcast + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default (by the OS) is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Sending + +/** + * Asynchronously sends the given data, with the given timeout and tag. + * + * This method may only be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param host + * The destination to send the udp packet to. + * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * You may also use the convenience strings of "loopback" or "localhost". + * + * @param port + * The port of the host to send to. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given address. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param remoteAddr + * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object). + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Receiving + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode. + * + * When a packet is received (and not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)receiveOnce:(NSError **)errPtr; + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode. + * + * For every received packet (not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)beginReceiving:(NSError **)errPtr; + +/** + * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving. + * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again. + * + * Important Note: + * GCDAsyncUdpSocket may be running in parallel with your code. + * That is, your delegate is likely running on a separate thread/dispatch_queue. + * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked. + * Thus, if those delegate methods have already been dispatch_async'd, + * your didReceive delegate method may still be invoked after this method has been called. + * You should be aware of this, and program defensively. +**/ +- (void)pauseReceiving; + +/** + * You may optionally set a receive filter for the socket. + * This receive filter may be set to run in its own queue (independent of delegate queue). + * + * A filter can provide several useful features. + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * + * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Closing + +/** + * Immediately closes the underlying socket. + * Any pending send operations are discarded. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)close; + +/** + * Closes the underlying socket after all pending send operations have been sent. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)closeAfterSending; + +#pragma mark Advanced +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket isn't connected, or explicity bound to a particular interface, + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket. + * + * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.) + * However, if you need one for any reason, + * these methods are a convenient way to get access to a safe instance of one. +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Example usage: + * + * [asyncUdpSocket performBlock:^{ + * [asyncUdpSocket enableBackgroundingOnSocket]; + * }]; + * + * + * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now). +**/ +//- (BOOL)enableBackgroundingOnSockets; + +#endif + +#pragma mark Utilities + +/** + * Extracting host/port/family information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; ++ (int)familyFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr fromAddress:(NSData *)address; ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr family:(int * __nullable)afPtr fromAddress:(NSData *)address; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m new file mode 100755 index 0000000..8972caa --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m @@ -0,0 +1,5642 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncUdpSocket.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + +#if TARGET_OS_IPHONE + #import + #import +#endif + +#import +#import +#import +#import +#import +#import +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +#if 0 + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync NO +#define LogContext 65535 + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +// Log levels : off, error, warn, info, verbose +static const int logLevel = LOG_LEVEL_VERBOSE; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + +/** + * Just to type less code. +**/ +#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} + + +@class GCDAsyncUdpSendPacket; + +NSString *const GCDAsyncUdpSocketException = @"GCDAsyncUdpSocketException"; +NSString *const GCDAsyncUdpSocketErrorDomain = @"GCDAsyncUdpSocketErrorDomain"; + +NSString *const GCDAsyncUdpSocketQueueName = @"GCDAsyncUdpSocket"; +NSString *const GCDAsyncUdpSocketThreadName = @"GCDAsyncUdpSocket-CFStream"; + +enum GCDAsyncUdpSocketFlags +{ + kDidCreateSockets = 1 << 0, // If set, the sockets have been created. + kDidBind = 1 << 1, // If set, bind has been called. + kConnecting = 1 << 2, // If set, a connection attempt is in progress. + kDidConnect = 1 << 3, // If set, socket is connected. + kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled + kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled + kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6. + kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4. + kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended. + kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended. + kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended. + kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended. + kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown. + kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown. + kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued. + kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued. + kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets. +#if TARGET_OS_IPHONE + kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread +#endif +}; + +enum GCDAsyncUdpSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6 + kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4 +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncUdpSocket () +{ +#if __has_feature(objc_arc_weak) + __weak id delegate; +#else + __unsafe_unretained id delegate; +#endif + dispatch_queue_t delegateQueue; + + GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; + dispatch_queue_t receiveFilterQueue; + BOOL receiveFilterAsync; + + GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; + dispatch_queue_t sendFilterQueue; + BOOL sendFilterAsync; + + uint32_t flags; + uint16_t config; + + uint16_t max4ReceiveSize; + uint32_t max6ReceiveSize; + + uint16_t maxSendSize; + + int socket4FD; + int socket6FD; + + dispatch_queue_t socketQueue; + + dispatch_source_t send4Source; + dispatch_source_t send6Source; + dispatch_source_t receive4Source; + dispatch_source_t receive6Source; + dispatch_source_t sendTimer; + + GCDAsyncUdpSendPacket *currentSend; + NSMutableArray *sendQueue; + + unsigned long socket4FDBytesAvailable; + unsigned long socket6FDBytesAvailable; + + uint32_t pendingFilterOperations; + + NSData *cachedLocalAddress4; + NSString *cachedLocalHost4; + uint16_t cachedLocalPort4; + + NSData *cachedLocalAddress6; + NSString *cachedLocalHost6; + uint16_t cachedLocalPort6; + + NSData *cachedConnectedAddress; + NSString *cachedConnectedHost; + uint16_t cachedConnectedPort; + int cachedConnectedFamily; + + void *IsOnSocketQueueOrTargetQueueKey; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream4; + CFReadStreamRef readStream6; + CFWriteStreamRef writeStream4; + CFWriteStreamRef writeStream6; +#endif + + id userData; +} + +- (void)resumeSend4Source; +- (void)resumeSend6Source; +- (void)resumeReceive4Source; +- (void)resumeReceive6Source; +- (void)closeSockets; + +- (void)maybeConnect; +- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr; +- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr; + +- (void)maybeDequeueSend; +- (void)doPreSend; +- (void)doSend; +- (void)endCurrentSend; +- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout; + +- (void)doReceive; +- (void)doReceiveEOF; + +- (void)closeWithError:(NSError *)error; + +- (BOOL)performMulticastRequest:(int)requestType forGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr; + +#if TARGET_OS_IPHONE +- (BOOL)createReadAndWriteStreams:(NSError **)errPtr; +- (BOOL)registerForStreamCallbacks:(NSError **)errPtr; +- (BOOL)addStreamsToRunLoop:(NSError **)errPtr; +- (BOOL)openStreams:(NSError **)errPtr; +- (void)removeStreamsFromRunLoop; +- (void)closeReadAndWriteStreams; +#endif + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; + +#if TARGET_OS_IPHONE +// Forward declaration ++ (void)listenerThread:(id)unused; +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncUdpSendPacket encompasses the instructions for a single send/write. +**/ +@interface GCDAsyncUdpSendPacket : NSObject { +@public + NSData *buffer; + NSTimeInterval timeout; + long tag; + + BOOL resolveInProgress; + BOOL filterInProgress; + + NSArray *resolvedAddresses; + NSError *resolveError; + + NSData *address; + int addressFamily; +} + +- (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GCDAsyncUdpSendPacket + +// Cover the superclass' designated initializer +- (instancetype)init NS_UNAVAILABLE +{ + NSAssert(0, @"Use the designated initializer"); + return nil; +} + +- (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if ((self = [super init])) + { + buffer = d; + timeout = t; + tag = i; + + resolveInProgress = NO; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncUdpSpecialPacket : NSObject { +@public +// uint8_t type; + + BOOL resolveInProgress; + + NSArray *addresses; + NSError *error; +} + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GCDAsyncUdpSpecialPacket + +- (instancetype)init +{ + self = [super init]; + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncUdpSocket + +- (instancetype)init +{ + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (instancetype)initWithSocketQueue:(dispatch_queue_t)sq +{ + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + LogTrace(); + + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + LogTrace(); + + if ((self = [super init])) + { + delegate = aDelegate; + + if (dq) + { + delegateQueue = dq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); + #endif + } + + max4ReceiveSize = 65535; + max6ReceiveSize = 65535; + + maxSendSize = 65535; + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(socketQueue); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + currentSend = nil; + sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; + + #if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + #endif + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#endif + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result = nil; + + dispatch_sync(socketQueue, ^{ + result = self->delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + self->delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result = NULL; + + dispatch_sync(socketQueue, ^{ + result = self->delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = self->delegate; + dqPtr = self->delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + self->delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((self->config & kIPv4Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + self->config &= ~kIPv4Disabled; + else + self->config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((self->config & kIPv6Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + self->config &= ~kIPv6Disabled; + else + self->config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4Preferred +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & kPreferIPv4) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv6Preferred +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & kPreferIPv6) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPVersionNeutral +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & (kPreferIPv4 | kPreferIPv6)) == 0; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setPreferIPv4 +{ + dispatch_block_t block = ^{ + + LogTrace(); + + self->config |= kPreferIPv4; + self->config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setPreferIPv6 +{ + dispatch_block_t block = ^{ + + LogTrace(); + + self->config &= ~kPreferIPv4; + self->config |= kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setIPVersionNeutral +{ + dispatch_block_t block = ^{ + + LogTrace(); + + self->config &= ~kPreferIPv4; + self->config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (uint16_t)maxReceiveIPv4BufferSize +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + result = self->max4ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max +{ + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->max4ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (uint32_t)maxReceiveIPv6BufferSize +{ + __block uint32_t result = 0; + + dispatch_block_t block = ^{ + + result = self->max6ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max +{ + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->max6ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setMaxSendBufferSize:(uint16_t)max +{ + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->maxSendSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (uint16_t)maxSendBufferSize +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + result = self->maxSendSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = self->userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (self->userData != arbitraryUserData) + { + self->userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Delegate Helpers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)notifyDidConnectToAddress:(NSData *)anAddress +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) + { + NSData *address = [anAddress copy]; // In case param is NSMutableData + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didConnectToAddress:address]; + }}); + } +} + +- (void)notifyDidNotConnect:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotConnect:error]; + }}); + } +} + +- (void)notifyDidSendDataWithTag:(long)tag +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didSendDataWithTag:tag]; + }}); + } +} + +- (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; + }}); + } +} + +- (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context +{ + LogTrace(); + + SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:selector]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; + }}); + } +} + +- (void)notifyDidCloseWithError:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocketDidClose:self withError:error]; + }}); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadConfigError + userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadParamError + userInfo:userInfo]; +} + +- (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo; + + if (reason) + userInfo = @{NSLocalizedDescriptionKey : errMsg, + NSLocalizedFailureReasonErrorKey : reason}; + else + userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + return [self errnoErrorWithReason:nil]; +} + +/** + * Returns a standard send timeout error. +**/ +- (NSError *)sendTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", + @"GCDAsyncUdpSocket", [NSBundle mainBundle], + @"Send operation timed out", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketSendTimeoutError + userInfo:userInfo]; +} + +- (NSError *)socketClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", + @"GCDAsyncUdpSocket", [NSBundle mainBundle], + @"Socket closed", nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketOtherError + userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)preOp:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +/** + * This method executes on a global concurrent queue. + * When complete, it executes the given completion block on the socketQueue. +**/ +- (void)asyncResolveHost:(NSString *)aHost + port:(uint16_t)port + withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock +{ + LogTrace(); + + // Check parameter(s) + + if (aHost == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + NSError *error = [self badParamError:msg]; + + // We should still use dispatch_async since this method is expected to be asynchronous + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + completionBlock(nil, error); + }}); + + return; + } + + // It's possible that the given aHost parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *host = [aHost copy]; + + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + + NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + // Wrap the native address structures and add to list + [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; + [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + for(res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address + // Wrap the native address structure and add to list + + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + else if (res->ai_family == AF_INET6) + { + + // Fixes connection issues with IPv6, it is the same solution for udp socket. + // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; + in_port_t *portPtr = &sockaddr->sin6_port; + if ((portPtr != NULL) && (*portPtr == 0)) { + *portPtr = htons(port); + } + + // Found IPv6 address + // Wrap the native address structure and add to list + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + completionBlock(addresses, error); + }}); + + }}); +} + +/** + * This method picks an address from the given list of addresses. + * The address picked depends upon which protocols are disabled, deactived, & preferred. + * + * Returns the address family (AF_INET or AF_INET6) of the picked address, + * or AF_UNSPEC and the corresponding error is there's a problem. +**/ +- (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses:(NSArray *)addresses +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert([addresses count] > 0, @"Expected at least one address"); + + int resultAF = AF_UNSPEC; + NSData *resultAddress = nil; + NSError *resultError = nil; + + // Check for problems + + BOOL resolvedIPv4Address = NO; + BOOL resolvedIPv6Address = NO; + + for (NSData *address in addresses) + { + switch ([[self class] familyFromAddress:address]) + { + case AF_INET : resolvedIPv4Address = YES; break; + case AF_INET6 : resolvedIPv6Address = YES; break; + + default : NSAssert(NO, @"Addresses array contains invalid address"); + } + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Disabled && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; + BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; + + if (isIPv4Deactivated && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Deactivated && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + // Extract first IPv4 and IPv6 address in list + + BOOL ipv4WasFirstInList = YES; + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + int af = [[self class] familyFromAddress:address]; + + if (af == AF_INET) + { + if (address4 == nil) + { + address4 = address; + + if (address6) + break; + else + ipv4WasFirstInList = YES; + } + } + else // af == AF_INET6 + { + if (address6 == nil) + { + address6 = address; + + if (address4) + break; + else + ipv4WasFirstInList = NO; + } + } + } + + // Determine socket type + + BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); + NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); + + if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) + { + resultAF = AF_INET; + resultAddress = address4; + } + else + { + resultAF = AF_INET6; + resultAddress = address6; + } + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; +} + +/** + * Finds the address(es) of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). +**/ +- (void)convertIntefaceDescription:(NSString *)interfaceDescription + port:(uint16_t)port + intoAddress4:(NSData **)interfaceAddr4Ptr + address6:(NSData **)interfaceAddr6Ptr +{ + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (interfaceDescription == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interfaceDescription isEqualToString:@"localhost"] || + [interfaceDescription isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interfaceDescription UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in *addr = (struct sockaddr_in *)(void *)cursor->ifa_addr; + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); + + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion; + conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); + + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion; + conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +/** + * Converts a numeric hostname into its corresponding address. + * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34) +**/ +- (void)convertNumericHost:(NSString *)numericHost + port:(uint16_t)port + intoAddress4:(NSData **)addr4Ptr + address6:(NSData **)addr6Ptr +{ + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (numericHost) + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted + + if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0) + { + for (res = res0; res; res = res->ai_next) + { + if ((addr4 == nil) && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structure + addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if ((addr6 == nil) && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structure + addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + } + + if (addr4Ptr) *addr4Ptr = addr4; + if (addr6Ptr) *addr6Ptr = addr6; +} + +- (BOOL)isConnectedToAddress4:(NSData *)someAddr4 +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET) + { + return NO; + } + + const struct sockaddr_in *sSockaddr4 = (const struct sockaddr_in *)[someAddr4 bytes]; + const struct sockaddr_in *cSockaddr4 = (const struct sockaddr_in *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr4->sin_port, &cSockaddr4->sin_port, sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; +} + +- (BOOL)isConnectedToAddress6:(NSData *)someAddr6 +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET6) + { + return NO; + } + + const struct sockaddr_in6 *sSockaddr6 = (const struct sockaddr_in6 *)[someAddr6 bytes]; + const struct sockaddr_in6 *cSockaddr6 = (const struct sockaddr_in6 *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr6->sin6_port, &cSockaddr6->sin6_port, sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; +} + +- (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 +{ + if (interfaceAddr4 == nil) + return 0; + if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) + return 0; + + int result = 0; + const struct sockaddr_in *ifaceAddr = (const struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET) + { + // IPv4 + + const struct sockaddr_in *addr = (const struct sockaddr_in *)(const void *)cursor->ifa_addr; + + if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0) + { + result = if_nametoindex(cursor->ifa_name); + break; + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 +{ + if (interfaceAddr6 == nil) + return 0; + if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) + return 0; + + int result = 0; + const struct sockaddr_in6 *ifaceAddr = (const struct sockaddr_in6 *)[interfaceAddr6 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET6) + { + // IPv6 + + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; + + if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0) + { + result = if_nametoindex(cursor->ifa_name); + break; + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (void)setupSendAndReceiveSourcesForSocket4 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue); + receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool { + + LogVerbose(@"send4EventBlock"); + LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source)); + + self->flags |= kSock4CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (self->currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend4Source]; + } + else if (self->currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend4Source]; + } + else if (self->currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend4Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool { + + LogVerbose(@"receive4EventBlock"); + + self->socket4FDBytesAvailable = dispatch_source_get_data(self->receive4Source); + LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); + + if (self->socket4FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket4FD; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send4Source; + dispatch_source_t theReceiveSource = receive4Source; + #endif + + dispatch_source_set_cancel_handler(send4Source, ^{ + + LogVerbose(@"send4CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send4Source)"); + dispatch_release(theSendSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive4Source, ^{ + + LogVerbose(@"receive4CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive4Source)"); + dispatch_release(theReceiveSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket4FDBytesAvailable = 0; + flags |= kSock4CanAcceptBytes; + + flags |= kSend4SourceSuspended; + flags |= kReceive4SourceSuspended; +} + +- (void)setupSendAndReceiveSourcesForSocket6 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue); + receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool { + + LogVerbose(@"send6EventBlock"); + LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source)); + + self->flags |= kSock6CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (self->currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend6Source]; + } + else if (self->currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend6Source]; + } + else if (self->currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend6Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool { + + LogVerbose(@"receive6EventBlock"); + + self->socket6FDBytesAvailable = dispatch_source_get_data(self->receive6Source); + LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); + + if (self->socket6FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket6FD; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send6Source; + dispatch_source_t theReceiveSource = receive6Source; + #endif + + dispatch_source_set_cancel_handler(send6Source, ^{ + + LogVerbose(@"send6CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send6Source)"); + dispatch_release(theSendSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive6Source, ^{ + + LogVerbose(@"receive6CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive6Source)"); + dispatch_release(theReceiveSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket6FDBytesAvailable = 0; + flags |= kSock6CanAcceptBytes; + + flags |= kSend6SourceSuspended; + flags |= kReceive6SourceSuspended; +} + +- (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __autoreleasing *)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created"); + + // CreateSocket Block + // This block will be invoked below. + + int(^createSocket)(int) = ^int (int domain) { + + int socketFD = socket(domain, SOCK_DGRAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int reuseaddr = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int nosigpipe = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + /** + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * The default maximum size of the UDP buffer in iOS is 9216 bytes. + * + * This is the reason of #222(GCD does not necessarily return the size of an entire UDP packet) and + * #535(GCDAsyncUDPSocket can not send data when data is greater than 9K) + * + * + * Enlarge the maximum size of UDP packet. + * I can not ensure the protocol type now so that the max size is set to 65535 :) + **/ + + status = setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, (const char*)&self->maxSendSize, sizeof(int)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error setting send buffer size (setsockopt)"]; + close(socketFD); + return SOCKET_NULL; + } + + status = setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, (const char*)&self->maxSendSize, sizeof(int)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error setting receive buffer size (setsockopt)"]; + close(socketFD); + return SOCKET_NULL; + } + + + return socketFD; + }; + + // Create sockets depending upon given configuration. + + if (useIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = createSocket(AF_INET); + if (socket4FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + return NO; + } + } + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = createSocket(AF_INET6); + if (socket6FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + + if (socket4FD != SOCKET_NULL) + { + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + return NO; + } + } + + // Setup send and receive sources + + if (useIPv4) + [self setupSendAndReceiveSourcesForSocket4]; + if (useIPv6) + [self setupSendAndReceiveSourcesForSocket6]; + + flags |= kDidCreateSockets; + return YES; +} + +- (BOOL)createSockets:(NSError **)errPtr +{ + LogTrace(); + + BOOL useIPv4 = [self isIPv4Enabled]; + BOOL useIPv6 = [self isIPv6Enabled]; + + return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; +} + +- (void)suspendSend4Source +{ + if (send4Source && !(flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send4Source)"); + + dispatch_suspend(send4Source); + flags |= kSend4SourceSuspended; + } +} + +- (void)suspendSend6Source +{ + if (send6Source && !(flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send6Source)"); + + dispatch_suspend(send6Source); + flags |= kSend6SourceSuspended; + } +} + +- (void)resumeSend4Source +{ + if (send4Source && (flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send4Source)"); + + dispatch_resume(send4Source); + flags &= ~kSend4SourceSuspended; + } +} + +- (void)resumeSend6Source +{ + if (send6Source && (flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send6Source)"); + + dispatch_resume(send6Source); + flags &= ~kSend6SourceSuspended; + } +} + +- (void)suspendReceive4Source +{ + if (receive4Source && !(flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive4Source)"); + + dispatch_suspend(receive4Source); + flags |= kReceive4SourceSuspended; + } +} + +- (void)suspendReceive6Source +{ + if (receive6Source && !(flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive6Source)"); + + dispatch_suspend(receive6Source); + flags |= kReceive6SourceSuspended; + } +} + +- (void)resumeReceive4Source +{ + if (receive4Source && (flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive4Source)"); + + dispatch_resume(receive4Source); + flags &= ~kReceive4SourceSuspended; + } +} + +- (void)resumeReceive6Source +{ + if (receive6Source && (flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive6Source)"); + + dispatch_resume(receive6Source); + flags &= ~kReceive6SourceSuspended; + } +} + +- (void)closeSocket4 +{ + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send4Source)"); + dispatch_source_cancel(send4Source); + + LogVerbose(@"dispatch_source_cancel(receive4Source)"); + dispatch_source_cancel(receive4Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend4Source]; + [self resumeReceive4Source]; + + // The sockets will be closed by the cancel handlers of the corresponding source + + send4Source = NULL; + receive4Source = NULL; + + socket4FD = SOCKET_NULL; + + // Clear socket states + + socket4FDBytesAvailable = 0; + flags &= ~kSock4CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress4 = nil; + cachedLocalHost4 = nil; + cachedLocalPort4 = 0; + } +} + +- (void)closeSocket6 +{ + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send6Source)"); + dispatch_source_cancel(send6Source); + + LogVerbose(@"dispatch_source_cancel(receive6Source)"); + dispatch_source_cancel(receive6Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend6Source]; + [self resumeReceive6Source]; + + send6Source = NULL; + receive6Source = NULL; + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket6FD = SOCKET_NULL; + + // Clear socket states + + socket6FDBytesAvailable = 0; + flags &= ~kSock6CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress6 = nil; + cachedLocalHost6 = nil; + cachedLocalPort6 = 0; + } +} + +- (void)closeSockets +{ + [self closeSocket4]; + [self closeSocket6]; + + flags &= ~kDidCreateSockets; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)getLocalAddress:(NSData **)dataPtr + host:(NSString **)hostPtr + port:(uint16_t *)portPtr + forSocket:(int)socketFD + withFamily:(int)socketFamily +{ + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + + if (socketFamily == AF_INET) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + else if (socketFamily == AF_INET6) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + + if (dataPtr) *dataPtr = data; + if (hostPtr) *hostPtr = host; + if (portPtr) *portPtr = port; + + return (data != nil); +} + +- (void)maybeUpdateCachedLocalAddress4Info +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) + { + + cachedLocalAddress4 = address; + cachedLocalHost4 = host; + cachedLocalPort4 = port; + } +} + +- (void)maybeUpdateCachedLocalAddress6Info +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) + { + + cachedLocalAddress6 = address; + cachedLocalHost6 = host; + cachedLocalPort6 = port; + } +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalAddress4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalAddress6; + } + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalHost4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalHost6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalPort4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalPort6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSData *)localAddress_IPv4 +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalAddress4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost_IPv4 +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalHost4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort_IPv4 +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalPort4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSData *)localAddress_IPv6 +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalAddress6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost_IPv6 +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalHost6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort_IPv6 +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalPort6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (void)maybeUpdateCachedConnectedAddressInfo +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (cachedConnectedAddress || (flags & kDidConnect) == 0) + { + return; + } + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + int family = AF_UNSPEC; + + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + family = AF_INET; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + else if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + family = AF_INET6; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + + + cachedConnectedAddress = data; + cachedConnectedHost = host; + cachedConnectedPort = port; + cachedConnectedFamily = family; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedAddress; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)connectedHost +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedHost; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)connectedPort +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedPort; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kDidConnect) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isClosed +{ + __block BOOL result = YES; + + dispatch_block_t block = ^{ + + result = (self->flags & kDidCreateSockets) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (self->flags & kDidCreateSockets) + { + result = (self->socket4FD != SOCKET_NULL); + } + else + { + result = [self isIPv4Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv6 +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (self->flags & kDidCreateSockets) + { + result = (self->socket6FD != SOCKET_NULL); + } + else + { + result = [self isIPv6Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Binding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a bind attempt. + * It is shared between the various bind methods. +**/ +- (BOOL)preBind:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if (flags & kDidBind) + { + if (errPtr) + { + NSString *msg = @"Cannot bind a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self bindToPort:port interface:nil error:errPtr]; +} + +- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given interface + + NSData *interface4 = nil; + NSData *interface6 = nil; + + [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); + + if (useIPv4) + { + int status = bind(self->socket4FD, (const struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + if (useIPv6) + { + int status = bind(self->socket6FD, (const struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + self->flags |= kDidBind; + + if (!useIPv4) self->flags |= kIPv4Deactivated; + if (!useIPv6) self->flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to port/interface: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given address + + int addressFamily = [[self class] familyFromAddress:localAddr]; + + if (addressFamily == AF_UNSPEC) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; + NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && localAddr4) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && localAddr6) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + if (useIPv4) + { + LogVerbose(@"Binding socket to address(%@:%hu)", + [[self class] hostFromAddress:localAddr4], + [[self class] portFromAddress:localAddr4]); + + int status = bind(self->socket4FD, (const struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + else + { + LogVerbose(@"Binding socket to address(%@:%hu)", + [[self class] hostFromAddress:localAddr6], + [[self class] portFromAddress:localAddr6]); + + int status = bind(self->socket6FD, (const struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + self->flags |= kDidBind; + + if (!useIPv4) self->flags |= kIPv4Deactivated; + if (!useIPv6) self->flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connect attempt. + * It is shared between the various connect methods. +**/ +- (BOOL)preConnect:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot connect a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (host == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // Create special connect packet + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->resolveInProgress = YES; + + // Start asynchronous DNS resolve for host:port on background queue + + LogVerbose(@"Dispatching DNS resolve for connect..."); + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->addresses = addresses; + packet->error = error; + + [self maybeConnect]; + }]; + + // Updates flags, add connect packet to send queue, and pump send queue + + self->flags |= kConnecting; + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to host/port: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (remoteAddr == nil) + { + NSString *msg = @"The address param is nil. Should be a valid address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // The remoteAddr parameter could be of type NSMutableData. + // So we copy it to be safe. + + NSData *address = [remoteAddr copy]; + NSArray *addresses = [NSArray arrayWithObject:address]; + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->addresses = addresses; + + // Updates flags, add connect packet to send queue, and pump send queue + + self->flags |= kConnecting; + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (void)maybeConnect +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; + + if (sendQueueReady) + { + GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; + + if (connectPacket->resolveInProgress) + { + LogVerbose(@"Waiting for DNS resolve..."); + } + else + { + if (connectPacket->error) + { + [self notifyDidNotConnect:connectPacket->error]; + } + else + { + NSData *address = nil; + NSError *error = nil; + + int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; + + // Perform connect + + BOOL result = NO; + + switch (addressFamily) + { + case AF_INET : result = [self connectWithAddress4:address error:&error]; break; + case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; + } + + if (result) + { + flags |= kDidBind; + flags |= kDidConnect; + + cachedConnectedAddress = address; + cachedConnectedHost = [[self class] hostFromAddress:address]; + cachedConnectedPort = [[self class] portFromAddress:address]; + cachedConnectedFamily = addressFamily; + + [self notifyDidConnectToAddress:address]; + } + else + { + [self notifyDidNotConnect:error]; + } + } + + flags &= ~kConnecting; + + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } +} + +- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + int status = connect(socket4FD, (const struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket6]; + flags |= kIPv6Deactivated; + + return YES; +} + +- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + int status = connect(socket6FD, (const struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket4]; + flags |= kIPv4Deactivated; + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Multicast +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)preJoin:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if (!(flags & kDidBind)) + { + if (errPtr) + { + NSString *msg = @"Must bind a socket before joining a multicast group."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot join a multicast group if connected."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr +{ + return [self joinMulticastGroup:group onInterface:nil error:errPtr]; +} + +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr +{ + // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP + return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; +} + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr +{ + return [self leaveMulticastGroup:group onInterface:nil error:errPtr]; +} + +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr +{ + // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP + return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; +} + +- (BOOL)performMulticastRequest:(int)requestType + forGroup:(NSString *)group + onInterface:(NSString *)interface + error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preJoin:&err]) + { + return_from_block; + } + + // Convert group to address + + NSData *groupAddr4 = nil; + NSData *groupAddr6 = nil; + + [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; + + if ((groupAddr4 == nil) && (groupAddr6 == nil)) + { + NSString *msg = @"Unknown group. Specify valid group IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Convert interface to address + + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; + + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + + if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Perform join + + if ((self->socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) + { + const struct sockaddr_in *nativeGroup = (const struct sockaddr_in *)[groupAddr4 bytes]; + const struct sockaddr_in *nativeIface = (const struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ip_mreq imreq; + imreq.imr_multiaddr = nativeGroup->sin_addr; + imreq.imr_interface = nativeIface->sin_addr; + + int status = setsockopt(self->socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv4 only + [self closeSocket6]; + + result = YES; + } + else if ((self->socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) + { + const struct sockaddr_in6 *nativeGroup = (const struct sockaddr_in6 *)[groupAddr6 bytes]; + + struct ipv6_mreq imreq; + imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; + imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; + + int status = setsockopt(self->socket6FD, IPPROTO_IPV6, requestType, (const void *)&imreq, sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv6 only + [self closeSocket4]; + + result = YES; + } + else + { + NSString *msg = @"Socket, group, and interface do not have matching IP versions"; + err = [self badParamError:msg]; + + return_from_block; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // Convert interface to address + + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; + + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + + if (interfaceAddr4 == nil) + { + NSString *msg = @"Unknown interface. Specify valid interface by IP address."; + err = [self badParamError:msg]; + return_from_block; + } + + if (self->socket4FD != SOCKET_NULL) { + const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes]; + struct in_addr interface_addr = nativeIface->sin_addr; + int status = setsockopt(self->socket4FD, IPPROTO_IP, IP_MULTICAST_IF, &interface_addr, sizeof(interface_addr)); + if (status != 0) { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + return_from_block; + result = YES; + } + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // Convert interface to address + + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; + + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + + if (interfaceAddr6 == nil) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\")."; + err = [self badParamError:msg]; + return_from_block; + } + + if ((self->socket6FD != SOCKET_NULL)) { + uint32_t scope_id = [self indexOfInterfaceAddr6:interfaceAddr6]; + int status = setsockopt(self->socket6FD, IPPROTO_IPV6, IPV6_MULTICAST_IF, &scope_id, sizeof(scope_id)); + if (status != 0) { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + return_from_block; + } + result = YES; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reuse port +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + int value = flag ? 1 : 0; + if (self->socket4FD != SOCKET_NULL) + { + int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + if (self->socket6FD != SOCKET_NULL) + { + int error = setsockopt(self->socket6FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Broadcast +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + if (self->socket4FD != SOCKET_NULL) + { + int value = flag ? 1 : 0; + int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. + // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)sendData:(NSData *)data withTag:(long)tag +{ + [self sendData:data withTimeout:-1.0 tag:tag]; +} + +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); + +} + +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->resolveInProgress = YES; + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->resolvedAddresses = addresses; + packet->resolveError = error; + + if (packet == self->currentSend) + { + LogVerbose(@"currentSend - address resolved"); + [self doPreSend]; + } + }]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + }}); + +} + +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; + packet->address = remoteAddr; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); +} + +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue +{ + [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; +} + +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous +{ + GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); + #endif + } + + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (self->sendFilterQueue) dispatch_release(self->sendFilterQueue); + #endif + + self->sendFilterBlock = newFilterBlock; + self->sendFilterQueue = newFilterQueue; + self->sendFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)maybeDequeueSend +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we don't have a send operation already in progress + if (currentSend == nil) + { + // Create the sockets if needed + if ((flags & kDidCreateSockets) == 0) + { + NSError *err = nil; + if (![self createSockets:&err]) + { + [self closeWithError:err]; + return; + } + } + + while ([sendQueue count] > 0) + { + // Dequeue the next object in the queue + currentSend = [sendQueue objectAtIndex:0]; + [sendQueue removeObjectAtIndex:0]; + + if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) + { + [self maybeConnect]; + + return; // The maybeConnect method, if it connects, will invoke this method again + } + else if (currentSend->resolveError) + { + // Notify delegate + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; + + // Clear currentSend + currentSend = nil; + + continue; + } + else + { + // Start preprocessing checks on the send packet + [self doPreSend]; + + break; + } + } + + if ((currentSend == nil) && (flags & kCloseAfterSends)) + { + [self closeWithError:nil]; + } + } +} + +/** + * This method is called after a sendPacket has been dequeued. + * It performs various preprocessing checks on the packet, + * and queries the sendFilter (if set) to determine if the packet can be sent. + * + * If the packet passes all checks, it will be passed on to the doSend method. +**/ +- (void)doPreSend +{ + LogTrace(); + + // + // 1. Check for problems with send packet + // + + BOOL waitingForResolve = NO; + NSError *error = nil; + + if (flags & kDidConnect) + { + // Connected socket + + if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) + { + NSString *msg = @"Cannot specify destination of packet for connected socket"; + error = [self badConfigError:msg]; + } + else + { + currentSend->address = cachedConnectedAddress; + currentSend->addressFamily = cachedConnectedFamily; + } + } + else + { + // Non-Connected socket + + if (currentSend->resolveInProgress) + { + // We're waiting for the packet's destination to be resolved. + waitingForResolve = YES; + } + else if (currentSend->resolveError) + { + error = currentSend->resolveError; + } + else if (currentSend->address == nil) + { + if (currentSend->resolvedAddresses == nil) + { + NSString *msg = @"You must specify destination of packet for a non-connected socket"; + error = [self badConfigError:msg]; + } + else + { + // Pick the proper address to use (out of possibly several resolved addresses) + + NSData *address = nil; + int addressFamily = AF_UNSPEC; + + addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; + + currentSend->address = address; + currentSend->addressFamily = addressFamily; + } + } + } + + if (waitingForResolve) + { + // We're waiting for the packet's destination to be resolved. + + LogVerbose(@"currentSend - waiting for address resolve"); + + if (flags & kSock4CanAcceptBytes) { + [self suspendSend4Source]; + } + if (flags & kSock6CanAcceptBytes) { + [self suspendSend6Source]; + } + + return; + } + + if (error) + { + // Unable to send packet due to some error. + // Notify delegate and move on. + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; + [self endCurrentSend]; + [self maybeDequeueSend]; + + return; + } + + // + // 2. Query sendFilter (if applicable) + // + + if (sendFilterBlock && sendFilterQueue) + { + // Query sendFilter + + if (sendFilterAsync) + { + // Scenario 1 of 3 - Need to asynchronously query sendFilter + + currentSend->filterInProgress = YES; + GCDAsyncUdpSendPacket *sendPacket = currentSend; + + dispatch_async(sendFilterQueue, ^{ @autoreleasepool { + + BOOL allowed = self->sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag); + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + sendPacket->filterInProgress = NO; + if (sendPacket == self->currentSend) + { + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:self->currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + }}); + }}); + } + else + { + // Scenario 2 of 3 - Need to synchronously query sendFilter + + __block BOOL allowed = YES; + + dispatch_sync(sendFilterQueue, ^{ @autoreleasepool { + + allowed = self->sendFilterBlock(self->currentSend->buffer, self->currentSend->address, self->currentSend->tag); + }}); + + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + } + else // if (!sendFilterBlock || !sendFilterQueue) + { + // Scenario 3 of 3 - No sendFilter. Just go straight into sending. + + [self doSend]; + } +} + +/** + * This method performs the actual sending of data in the currentSend packet. + * It should only be called if the +**/ +- (void)doSend +{ + LogTrace(); + + NSAssert(currentSend != nil, @"Invalid logic"); + + // Perform the actual send + + ssize_t result = 0; + + if (flags & kDidConnect) + { + // Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + if (currentSend->addressFamily == AF_INET) + { + result = send(socket4FD, buffer, length, 0); + LogVerbose(@"send(socket4FD) = %d", result); + } + else + { + result = send(socket6FD, buffer, length, 0); + LogVerbose(@"send(socket6FD) = %d", result); + } + } + else + { + // Non-Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + const void *dst = [currentSend->address bytes]; + socklen_t dstSize = (socklen_t)[currentSend->address length]; + + if (currentSend->addressFamily == AF_INET) + { + result = sendto(socket4FD, buffer, length, 0, dst, dstSize); + LogVerbose(@"sendto(socket4FD) = %d", result); + } + else + { + result = sendto(socket6FD, buffer, length, 0, dst, dstSize); + LogVerbose(@"sendto(socket6FD) = %d", result); + } + } + + // If the socket wasn't bound before, it is now + + if ((flags & kDidBind) == 0) + { + flags |= kDidBind; + } + + // Check the results. + // + // From the send() & sendto() manpage: + // + // Upon successful completion, the number of bytes which were sent is returned. + // Otherwise, -1 is returned and the global variable errno is set to indicate the error. + + BOOL waitingForSocket = NO; + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in send() function."]; + } + + if (waitingForSocket) + { + // Not enough room in the underlying OS socket send buffer. + // Wait for a notification of available space. + + LogVerbose(@"currentSend - waiting for socket"); + + if (!(flags & kSock4CanAcceptBytes)) { + [self resumeSend4Source]; + } + if (!(flags & kSock6CanAcceptBytes)) { + [self resumeSend6Source]; + } + + if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) + { + // Unable to send packet right away. + // Start timer to timeout the send operation. + + [self setupSendTimerWithTimeout:currentSend->timeout]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else // done + { + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } +} + +/** + * Releases all resources associated with the currentSend. +**/ +- (void)endCurrentSend +{ + if (sendTimer) + { + dispatch_source_cancel(sendTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(sendTimer); + #endif + sendTimer = NULL; + } + + currentSend = nil; +} + +/** + * Performs the operations to timeout the current send operation, and move on. +**/ +- (void)doSendTimeout +{ + LogTrace(); + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; + [self endCurrentSend]; + [self maybeDequeueSend]; +} + +/** + * Sets up a timer that fires to timeout the current send operation. + * This method should only be called once per send packet. +**/ +- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout +{ + NSAssert(sendTimer == NULL, @"Invalid logic"); + NSAssert(timeout >= 0.0, @"Invalid logic"); + + LogTrace(); + + sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { + + [self doSendTimeout]; + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(sendTimer); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Receiving +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)receiveOnce:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((self->flags & kReceiveOnce) == 0) + { + if ((self->flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + self->flags |= kReceiveOnce; // Enable + self->flags &= ~kReceiveContinuous; // Disable + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)beginReceiving:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((self->flags & kReceiveContinuous) == 0) + { + if ((self->flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + self->flags |= kReceiveContinuous; // Enable + self->flags &= ~kReceiveOnce; // Disable + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (void)pauseReceiving +{ + LogTrace(); + + dispatch_block_t block = ^{ + + self->flags &= ~kReceiveOnce; // Disable + self->flags &= ~kReceiveContinuous; // Disable + + if (self->socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (self->socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue +{ + [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; +} + +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous +{ + GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); + #endif + } + + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (self->receiveFilterQueue) dispatch_release(self->receiveFilterQueue); + #endif + + self->receiveFilterBlock = newFilterBlock; + self->receiveFilterQueue = newFilterQueue; + self->receiveFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)doReceive +{ + LogTrace(); + + if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) + { + LogVerbose(@"Receiving is paused..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) + { + LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) + { + LogVerbose(@"No data available to receive..."); + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + + return; + } + + // Figure out if we should receive on socket4 or socket6 + + BOOL doReceive4; + + if (flags & kDidConnect) + { + // Connected socket + + doReceive4 = (socket4FD != SOCKET_NULL); + } + else + { + // Non-Connected socket + + if (socket4FDBytesAvailable > 0) + { + if (socket6FDBytesAvailable > 0) + { + // Bytes available on socket4 & socket6 + + doReceive4 = (flags & kFlipFlop) ? YES : NO; + + flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) + } + else { + // Bytes available on socket4, but not socket6 + doReceive4 = YES; + } + } + else { + // Bytes available on socket6, but not socket4 + doReceive4 = NO; + } + } + + // Perform socket IO + + ssize_t result = 0; + + NSData *data = nil; + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (doReceive4) + { + NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv4"); + + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max4ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); + LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket4FDBytesAvailable) + socket4FDBytesAvailable = 0; + else + socket4FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + } + else + { + LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]); + socket4FDBytesAvailable = 0; + free(buf); + } + } + else + { + NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv6"); + + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max6ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); + LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket6FDBytesAvailable) + socket6FDBytesAvailable = 0; + else + socket6FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + } + else + { + LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]); + socket6FDBytesAvailable = 0; + free(buf); + } + } + + + BOOL waitingForSocket = NO; + BOOL notifiedDelegate = NO; + BOOL ignored = NO; + + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"]; + } + else + { + if (flags & kDidConnect) + { + if (addr4 && ![self isConnectedToAddress4:addr4]) + ignored = YES; + if (addr6 && ![self isConnectedToAddress6:addr6]) + ignored = YES; + } + + NSData *addr = (addr4 != nil) ? addr4 : addr6; + + if (!ignored) + { + if (receiveFilterBlock && receiveFilterQueue) + { + // Run data through filter, and if approved, notify delegate + + __block id filterContext = nil; + __block BOOL allowed = NO; + + if (receiveFilterAsync) + { + pendingFilterOperations++; + dispatch_async(receiveFilterQueue, ^{ @autoreleasepool { + + allowed = self->receiveFilterBlock(data, addr, &filterContext); + + // Transition back to socketQueue to get the current delegate / delegateQueue + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + self->pendingFilterOperations--; + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + } + + if (self->flags & kReceiveOnce) + { + if (allowed) + { + // The delegate has been notified, + // so our receive once operation has completed. + self->flags &= ~kReceiveOnce; + } + else if (self->pendingFilterOperations == 0) + { + // All pending filter operations have completed, + // and none were allowed through. + // Our receive once operation hasn't completed yet. + [self doReceive]; + } + } + }}); + }}); + } + else // if (!receiveFilterAsync) + { + dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { + + allowed = self->receiveFilterBlock(data, addr, &filterContext); + }}); + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + notifiedDelegate = YES; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + ignored = YES; + } + } + } + else // if (!receiveFilterBlock || !receiveFilterQueue) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil]; + notifiedDelegate = YES; + } + } + } + + if (waitingForSocket) + { + // Wait for a notification of available data. + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else + { + if (flags & kReceiveContinuous) + { + // Continuous receive mode + [self doReceive]; + } + else + { + // One-at-a-time receive mode + if (notifiedDelegate) + { + // The delegate has been notified (no set filter). + // So our receive once operation has completed. + flags &= ~kReceiveOnce; + } + else if (ignored) + { + [self doReceive]; + } + else + { + // Waiting on asynchronous receive filter... + } + } + } +} + +- (void)doReceiveEOF +{ + LogTrace(); + + [self closeWithError:[self socketClosedError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Closing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogVerbose(@"closeWithError: %@", error); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (currentSend) [self endCurrentSend]; + + [sendQueue removeAllObjects]; + + // If a socket has been created, we should notify the delegate. + BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; + + // Close all sockets, send/receive sources, cfstreams, etc +#if TARGET_OS_IPHONE + [self removeStreamsFromRunLoop]; + [self closeReadAndWriteStreams]; +#endif + [self closeSockets]; + + // Clear all flags (config remains as is) + flags = 0; + + if (shouldCallDelegate) + { + [self notifyDidCloseWithError:error]; + } +} + +- (void)close +{ + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [self closeWithError:nil]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)closeAfterSending +{ + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + self->flags |= kCloseAfterSends; + + if (self->currentSend == nil && [self->sendQueue count] == 0) + { + [self closeWithError:nil]; + } + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +static NSThread *listenerThread; + ++ (void)ignore:(id)_ +{} + ++ (void)startListenerThreadIfNeeded +{ + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + listenerThread = [[NSThread alloc] initWithTarget:self + selector:@selector(listenerThread:) + object:nil]; + [listenerThread start]; + }); +} + ++ (void)listenerThread:(id)unused +{ + @autoreleasepool { + + [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; + + LogInfo(@"ListenerThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for a decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + [[NSRunLoop currentRunLoop] run]; + + LogInfo(@"ListenerThread: Stopped"); + } +} + ++ (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-enum" + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFReadStreamCallback - Open"); + break; + } + case kCFStreamEventHasBytesAvailable: + { + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->readStream4 && + stream != asyncUdpSocket->readStream6 ) + { + LogCVerbose(@"CFReadStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type); + } + } +#pragma clang diagnostic pop + } +} + +static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-enum" + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFWriteStreamCallback - Open"); + break; + } + case kCFStreamEventCanAcceptBytes: + { + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->writeStream4 && + stream != asyncUdpSocket->writeStream6 ) + { + LogCVerbose(@"CFWriteStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type); + } + } +#pragma clang diagnostic pop + } +} + +- (BOOL)createReadAndWriteStreams:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + NSError *err = nil; + + if (readStream4 || writeStream4 || readStream6 || writeStream6) + { + // Streams already created + return YES; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + err = [self otherError:@"Cannot create streams without a file descriptor"]; + goto Failed; + } + + // Create streams + + LogVerbose(@"Creating read and write stream(s)..."); + + if (socket4FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4); + if (!readStream4 || !writeStream4) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6); + if (!readStream6 || !writeStream6) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"]; + goto Failed; + } + } + + // Ensure the CFStream's don't close our underlying socket + + CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + return YES; + +Failed: + if (readStream4) + { + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } + + if (errPtr) + *errPtr = err; + + return NO; +} + +- (BOOL)registerForStreamCallbacks:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + NSError *err = nil; + + streamContext.version = 0; + streamContext.info = (__bridge void *)self; + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + +// readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); +// writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); + + if (socket4FD != SOCKET_NULL) + { + if (readStream4 == NULL || writeStream4 == NULL) + { + err = [self otherError:@"Read/Write stream4 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + if (readStream6 == NULL || writeStream6 == NULL) + { + err = [self otherError:@"Read/Write stream6 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; + goto Failed; + } + } + + return YES; + +Failed: + if (readStream4) { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + } + if (writeStream4) { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + } + if (readStream6) { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + } + if (writeStream6) { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + } + + if (errPtr) *errPtr = err; + return NO; +} + +- (BOOL)addStreamsToRunLoop:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + if (!(flags & kAddedStreamListener)) + { + [[self class] startListenerThreadIfNeeded]; + [[self class] performSelector:@selector(addStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamListener; + } + + return YES; +} + +- (BOOL)openStreams:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + NSError *err = nil; + + if (socket4FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream4); + BOOL r2 = CFWriteStreamOpen(writeStream4); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream6); + BOOL r2 = CFWriteStreamOpen(writeStream6); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; + goto Failed; + } + } + + return YES; + +Failed: + if (errPtr) *errPtr = err; + return NO; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (flags & kAddedStreamListener) + { + [[self class] performSelector:@selector(removeStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags &= ~kAddedStreamListener; + } +} + +- (void)closeReadAndWriteStreams +{ + LogTrace(); + + if (readStream4) + { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } +} + +#endif + +#if TARGET_OS_IPHONE +- (void)applicationWillEnterForeground:(NSNotification *)notification +{ + LogTrace(); + + // If the application was backgrounded, then iOS may have shut down our sockets. + // So we take a quick look to see if any of them received an EOF. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self resumeReceive4Source]; + [self resumeReceive6Source]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. + **/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (int)socketFD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +- (int)socket4FD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +- (int)socket6FD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +- (CFReadStreamRef)readStream +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + // Todo... + + if (readStream4) + return readStream4; + else + return readStream6; +} + +- (CFWriteStreamRef)writeStream +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + if (writeStream4) + return writeStream4; + else + return writeStream6; +} + +- (BOOL)enableBackgroundingOnSockets +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NO; + } + + // Why is this commented out? + // See comments below. + +// NSError *err = nil; +// if (![self createReadAndWriteStreams:&err]) +// { +// LogError(@"Error creating CFStream(s): %@", err); +// return NO; +// } +// +// LogVerbose(@"Enabling backgrouding on socket"); +// +// BOOL r1, r2; +// +// if (readStream4 && writeStream4) +// { +// r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// +// if (!r1 || !r2) +// { +// LogError(@"Error setting voip type (IPv4)"); +// return NO; +// } +// } +// +// if (readStream6 && writeStream6) +// { +// r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// +// if (!r1 || !r2) +// { +// LogError(@"Error setting voip type (IPv6)"); +// return NO; +// } +// } +// +// return YES; + + // The above code will actually appear to work. + // The methods will return YES, and everything will appear fine. + // + // One tiny problem: the sockets will still get closed when the app gets backgrounded. + // + // Apple does not officially support backgrounding UDP sockets. + + return NO; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host = nil; + [self getHost:&host port:NULL family:NULL fromAddress:address]; + + return host; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port = 0; + [self getHost:NULL port:&port family:NULL fromAddress:address]; + + return port; +} + ++ (int)familyFromAddress:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return af; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET); +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET6); +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; + + if (addrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + const struct sockaddr_in *addr4 = (const struct sockaddr_in *)(const void *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (addrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)(const void *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + if (hostPtr) *hostPtr = nil; + if (portPtr) *portPtr = 0; + if (afPtr) *afPtr = AF_UNSPEC; + + return NO; +} + +@end + +#pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h new file mode 100644 index 0000000..2643610 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h @@ -0,0 +1,12 @@ +#import + + +@interface NSNumber (DDNumber) + ++ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum; ++ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum; + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum; ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m new file mode 100644 index 0000000..2a9f207 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m @@ -0,0 +1,88 @@ +#import "DDNumber.h" + + +@implementation NSNumber (DDNumber) + ++ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, long long = 64 bit + + *pNum = strtoll([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, unsigned long long = 64 bit + + *pNum = strtoull([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On LP64, NSInteger = long = 64 bit + // Otherwise, NSInteger = int = long = 32 bit + + *pNum = strtol([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum +{ + if(str == nil) + { + *pNum = 0; + return NO; + } + + errno = 0; + + // On LP64, NSUInteger = unsigned long = 64 bit + // Otherwise, NSUInteger = unsigned int = unsigned long = 32 bit + + *pNum = strtoul([str UTF8String], NULL, 10); + + if(errno != 0) + return NO; + else + return YES; +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h new file mode 100644 index 0000000..e01db03 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h @@ -0,0 +1,56 @@ +/** + * DDRange is the functional equivalent of a 64 bit NSRange. + * The HTTP Server is designed to support very large files. + * On 32 bit architectures (ppc, i386) NSRange uses unsigned 32 bit integers. + * This only supports a range of up to 4 gigabytes. + * By defining our own variant, we can support a range up to 16 exabytes. + * + * All effort is given such that DDRange functions EXACTLY the same as NSRange. + **/ + +#import +#import + +@class NSString; + +typedef struct _DDRange { + UInt64 location; + UInt64 length; +} DDRange; + +typedef DDRange *DDRangePointer; + +NS_INLINE DDRange DDMakeRange(UInt64 loc, UInt64 len) { + DDRange r; + r.location = loc; + r.length = len; + return r; +} + +NS_INLINE UInt64 DDMaxRange(DDRange range) { + return (range.location + range.length); +} + +NS_INLINE BOOL DDLocationInRange(UInt64 loc, DDRange range) { + return (loc - range.location < range.length); +} + +NS_INLINE BOOL DDEqualRanges(DDRange range1, DDRange range2) { + return ((range1.location == range2.location) && (range1.length == range2.length)); +} + +FOUNDATION_EXPORT DDRange DDUnionRange(DDRange range1, DDRange range2); +FOUNDATION_EXPORT DDRange DDIntersectionRange(DDRange range1, DDRange range2); +FOUNDATION_EXPORT NSString *DDStringFromRange(DDRange range); +FOUNDATION_EXPORT DDRange DDRangeFromString(NSString *aString); + +NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2); + +@interface NSValue (NSValueDDRangeExtensions) + ++ (NSValue *)valueWithDDRange:(DDRange)range; +- (DDRange)ddrangeValue; + +- (NSInteger)ddrangeCompare:(NSValue *)ddrangeValue; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m new file mode 100644 index 0000000..4aef974 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m @@ -0,0 +1,106 @@ +#import "DDRange.h" +#import "DDNumber.h" + +#pragma clang diagnostic ignored "-Wformat-non-iso" + +DDRange DDUnionRange(DDRange range1, DDRange range2) +{ + DDRange result; + + result.location = MIN(range1.location, range2.location); + result.length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - result.location; + + return result; +} + +DDRange DDIntersectionRange(DDRange range1, DDRange range2) +{ + DDRange result; + + if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location)) + { + return DDMakeRange(0, 0); + } + + result.location = MAX(range1.location, range2.location); + result.length = MIN(DDMaxRange(range1), DDMaxRange(range2)) - result.location; + + return result; +} + +NSString *DDStringFromRange(DDRange range) +{ + return [NSString stringWithFormat:@"{%qu, %qu}", range.location, range.length]; +} + +DDRange DDRangeFromString(NSString *aString) +{ + DDRange result = DDMakeRange(0, 0); + + // NSRange will ignore '-' characters, but not '+' characters + NSCharacterSet *cset = [NSCharacterSet characterSetWithCharactersInString:@"+0123456789"]; + + NSScanner *scanner = [NSScanner scannerWithString:aString]; + [scanner setCharactersToBeSkipped:[cset invertedSet]]; + + NSString *str1 = nil; + NSString *str2 = nil; + + BOOL found1 = [scanner scanCharactersFromSet:cset intoString:&str1]; + BOOL found2 = [scanner scanCharactersFromSet:cset intoString:&str2]; + + if(found1) [NSNumber parseString:str1 intoUInt64:&result.location]; + if(found2) [NSNumber parseString:str2 intoUInt64:&result.length]; + + return result; +} + +NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2) +{ + // Comparison basis: + // Which range would you encouter first if you started at zero, and began walking towards infinity. + // If you encouter both ranges at the same time, which range would end first. + + if(pDDRange1->location < pDDRange2->location) + { + return NSOrderedAscending; + } + if(pDDRange1->location > pDDRange2->location) + { + return NSOrderedDescending; + } + if(pDDRange1->length < pDDRange2->length) + { + return NSOrderedAscending; + } + if(pDDRange1->length > pDDRange2->length) + { + return NSOrderedDescending; + } + + return NSOrderedSame; +} + +@implementation NSValue (NSValueDDRangeExtensions) + ++ (NSValue *)valueWithDDRange:(DDRange)range +{ + return [NSValue valueWithBytes:&range objCType:@encode(DDRange)]; +} + +- (DDRange)ddrangeValue +{ + DDRange result; + [self getValue:&result]; + return result; +} + +- (NSInteger)ddrangeCompare:(NSValue *)other +{ + DDRange r1 = [self ddrangeValue]; + DDRange r2 = [other ddrangeValue]; + + return DDRangeCompare(&r1, &r2); +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h new file mode 100644 index 0000000..e186853 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h @@ -0,0 +1,107 @@ +#import + +@class GCDAsyncSocket; +@class HTTPMessage; +@class HTTPServer; +@class WebSocket; +@protocol HTTPResponse; + + +#define HTTPConnectionDidDieNotification @"HTTPConnectionDidDie" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface HTTPConfig : NSObject +{ + HTTPServer __unsafe_unretained *server; + NSString __strong *documentRoot; + dispatch_queue_t queue; +} + +- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot; +- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot queue:(dispatch_queue_t)q; + +@property (nonatomic, unsafe_unretained, readonly) HTTPServer *server; +@property (nonatomic, strong, readonly) NSString *documentRoot; +@property (nonatomic, readonly) dispatch_queue_t queue; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface HTTPConnection : NSObject +{ + dispatch_queue_t connectionQueue; + GCDAsyncSocket *asyncSocket; + HTTPConfig *config; + + BOOL started; + + HTTPMessage *request; + unsigned int numHeaderLines; + + BOOL sentResponseHeaders; + + NSObject *httpResponse; + + NSMutableArray *ranges; + NSMutableArray *ranges_headers; + NSString *ranges_boundry; + int rangeIndex; + + UInt64 requestContentLength; + UInt64 requestContentLengthReceived; + UInt64 requestChunkSize; + UInt64 requestChunkSizeReceived; + + NSMutableArray *responseDataSizes; +} + +- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig; + +- (void)start; +- (void)stop; + +- (void)startConnection; + +- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path; +- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path; + +- (NSDictionary *)parseParams:(NSString *)query; +- (NSDictionary *)parseGetParams; + +- (NSString *)requestURI; + +- (NSArray *)directoryIndexFileNames; +- (NSString *)filePathForURI:(NSString *)path; +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory; +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path; +- (WebSocket *)webSocketForURI:(NSString *)path; + +- (void)prepareForBodyWithSize:(UInt64)contentLength; +- (void)processBodyData:(NSData *)postDataChunk; +- (void)finishBody; + +- (void)handleVersionNotSupported:(NSString *)version; +- (void)handleResourceNotFound; +- (void)handleInvalidRequest:(NSData *)data; +- (void)handleUnknownMethod:(NSString *)method; + +- (NSData *)preprocessResponse:(HTTPMessage *)response; +- (NSData *)preprocessErrorResponse:(HTTPMessage *)response; + +- (void)finishResponse; + +- (BOOL)shouldDie; +- (void)die; + +@end + +@interface HTTPConnection (AsynchronousHTTPResponse) +- (void)responseHasAvailableData:(NSObject *)sender; +- (void)responseDidAbort:(NSObject *)sender; +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m new file mode 100644 index 0000000..064f653 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m @@ -0,0 +1,2238 @@ +#import "HTTPServer.h" +#import "HTTPConnection.h" +#import "HTTPMessage.h" +#import "HTTPResponse.h" +#import "DDNumber.h" +#import "DDRange.h" +#import "HTTPLogging.h" + +#import "GCDAsyncSocket.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#pragma clang diagnostic ignored "-Wunknown-warning-option" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Wimplicit-retain-self" +#pragma clang diagnostic ignored "-Wformat-non-iso" +#pragma clang diagnostic ignored "-Wunused-variable" +#pragma clang diagnostic ignored "-Wsign-compare" +#pragma clang diagnostic ignored "-Wformat-nonliteral" +#pragma clang diagnostic ignored "-Wunreachable-code" +#pragma clang diagnostic ignored "-Wfloat-conversion" + +// Log levels: off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; + +// Define chunk size used to read in data for responses +// This is how much data will be read from disk into RAM at a time +#if TARGET_OS_IPHONE +#define READ_CHUNKSIZE (1024 * 256) +#else +#define READ_CHUNKSIZE (1024 * 512) +#endif + +// Define chunk size used to read in POST upload data +#if TARGET_OS_IPHONE +#define POST_CHUNKSIZE (1024 * 256) +#else +#define POST_CHUNKSIZE (1024 * 512) +#endif + +// Define the various timeouts (in seconds) for various parts of the HTTP process +#define TIMEOUT_READ_FIRST_HEADER_LINE 30 +#define TIMEOUT_READ_SUBSEQUENT_HEADER_LINE 30 +#define TIMEOUT_READ_BODY -1 +#define TIMEOUT_WRITE_HEAD 30 +#define TIMEOUT_WRITE_BODY -1 +#define TIMEOUT_WRITE_ERROR 30 +#define TIMEOUT_NONCE 300 + +// Define the various limits +// MAX_HEADER_LINE_LENGTH: Max length (in bytes) of any single line in a header (including \r\n) +// MAX_HEADER_LINES : Max number of lines in a single header (including first GET line) +#define MAX_HEADER_LINE_LENGTH 8190 +#define MAX_HEADER_LINES 100 +// MAX_CHUNK_LINE_LENGTH : For accepting chunked transfer uploads, max length of chunk size line (including \r\n) +#define MAX_CHUNK_LINE_LENGTH 200 + +// Define the various tags we'll use to differentiate what it is we're currently doing +#define HTTP_REQUEST_HEADER 10 +#define HTTP_REQUEST_BODY 11 +#define HTTP_REQUEST_CHUNK_SIZE 12 +#define HTTP_REQUEST_CHUNK_DATA 13 +#define HTTP_REQUEST_CHUNK_TRAILER 14 +#define HTTP_REQUEST_CHUNK_FOOTER 15 +#define HTTP_PARTIAL_RESPONSE 20 +#define HTTP_PARTIAL_RESPONSE_HEADER 21 +#define HTTP_PARTIAL_RESPONSE_BODY 22 +#define HTTP_CHUNKED_RESPONSE_HEADER 30 +#define HTTP_CHUNKED_RESPONSE_BODY 31 +#define HTTP_CHUNKED_RESPONSE_FOOTER 32 +#define HTTP_PARTIAL_RANGE_RESPONSE_BODY 40 +#define HTTP_PARTIAL_RANGES_RESPONSE_BODY 50 +#define HTTP_RESPONSE 90 +#define HTTP_FINAL_RESPONSE 91 + +// A quick note about the tags: +// +// The HTTP_RESPONSE and HTTP_FINAL_RESPONSE are designated tags signalling that the response is completely sent. +// That is, in the onSocket:didWriteDataWithTag: method, if the tag is HTTP_RESPONSE or HTTP_FINAL_RESPONSE, +// it is assumed that the response is now completely sent. +// Use HTTP_RESPONSE if it's the end of a response, and you want to start reading more requests afterwards. +// Use HTTP_FINAL_RESPONSE if you wish to terminate the connection after sending the response. +// +// If you are sending multiple data segments in a custom response, make sure that only the last segment has +// the HTTP_RESPONSE tag. For all other segments prior to the last segment use HTTP_PARTIAL_RESPONSE, or some other +// tag of your own invention. + +@interface HTTPConnection (PrivateAPI) +- (void)startReadingRequest; +- (void)sendResponseHeadersAndBody; +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPConnection + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Init, Dealloc: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sole Constructor. + * Associates this new HTTP connection with the given AsyncSocket. + * This HTTP connection object will become the socket's delegate and take over responsibility for the socket. + **/ +- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + if (aConfig.queue) + { + connectionQueue = aConfig.queue; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(connectionQueue); +#endif + } + else + { + connectionQueue = dispatch_queue_create("HTTPConnection", NULL); + } + + // Take over ownership of the socket + asyncSocket = newSocket; + [asyncSocket setDelegate:(id)self delegateQueue:connectionQueue]; + + + // Store configuration + config = aConfig; + + // Create a new HTTP message + request = [[HTTPMessage alloc] initEmptyRequest]; + + numHeaderLines = 0; + + responseDataSizes = [[NSMutableArray alloc] initWithCapacity:5]; + } + return self; +} + +/** + * Standard Deconstructor. + **/ +- (void)dealloc +{ + HTTPLogTrace(); + +#if !OS_OBJECT_USE_OBJC + dispatch_release(connectionQueue); +#endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; + + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Method Support +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns whether or not the server will accept messages of a given method + * at a particular URI. + **/ +- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to support methods such as POST. + // + // Things you may want to consider: + // - Does the given path represent a resource that is designed to accept this method? + // - If accepting an upload, is the size of the data being uploaded too big? + // To do this you can check the requestContentLength variable. + // + // For more information, you can always access the HTTPMessage request variable. + // + // You should fall through with a call to [super supportsMethod:method atPath:path] + // + // See also: expectsRequestBodyFromMethod:atPath: + + if ([method isEqualToString:@"GET"]) + return YES; + + if ([method isEqualToString:@"HEAD"]) + return YES; + + return NO; +} + +/** + * Returns whether or not the server expects a body from the given method. + * + * In other words, should the server expect a content-length header and associated body from this method. + * This would be true in the case of a POST, where the client is sending data, + * or for something like PUT where the client is supposed to be uploading a file. + **/ +- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to add support for other methods that expect the client + // to send a body along with the request header. + // + // You should fall through with a call to [super expectsRequestBodyFromMethod:method atPath:path] + // + // See also: supportsMethod:atPath: + + if ([method isEqualToString:@"POST"]) + return YES; + + if ([method isEqualToString:@"PUT"]) + return YES; + + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Core +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starting point for the HTTP connection after it has been fully initialized (including subclasses). + * This method is called by the HTTP server. + **/ +- (void)start +{ + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (!started) + { + started = YES; + [self startConnection]; + } + }}); +} + +/** + * This method is called by the HTTPServer if it is asked to stop. + * The server, in turn, invokes stop on each HTTPConnection instance. + **/ +- (void)stop +{ + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + // Disconnect the socket. + // The socketDidDisconnect delegate method will handle everything else. + [asyncSocket disconnect]; + }}); +} + +/** + * Starting point for the HTTP connection. + **/ +- (void)startConnection +{ + // Override me to do any custom work before the connection starts. + // + // Be sure to invoke [super startConnection] when you're done. + + HTTPLogTrace(); + + [self startReadingRequest]; +} + +/** + * Starts reading an HTTP request. + **/ +- (void)startReadingRequest +{ + HTTPLogTrace(); + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_FIRST_HEADER_LINE + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_HEADER]; +} + +/** + * Parses the given query string. + * + * For example, if the query is "q=John%20Mayer%20Trio&num=50" + * then this method would return the following dictionary: + * { + * q = "John Mayer Trio" + * num = "50" + * } + **/ +- (NSDictionary *)parseParams:(NSString *)query +{ + NSArray *components = [query componentsSeparatedByString:@"&"]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[components count]]; + + NSUInteger i; + for (i = 0; i < [components count]; i++) + { + NSString *component = [components objectAtIndex:i]; + if ([component length] > 0) + { + NSRange range = [component rangeOfString:@"="]; + if (range.location != NSNotFound) + { + NSString *escapedKey = [component substringToIndex:(range.location + 0)]; + NSString *escapedValue = [component substringFromIndex:(range.location + 1)]; + + if ([escapedKey length] > 0) + { + CFStringRef k, v; + + k = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedKey, CFSTR("")); + v = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedValue, CFSTR("")); + + NSString *key, *value; + + key = (__bridge_transfer NSString *)k; + value = (__bridge_transfer NSString *)v; + + if (key) + { + if (value) + [result setObject:value forKey:key]; + else + [result setObject:[NSNull null] forKey:key]; + } + } + } + } + } + + return result; +} + +/** + * Parses the query variables in the request URI. + * + * For example, if the request URI was "/search.html?q=John%20Mayer%20Trio&num=50" + * then this method would return the following dictionary: + * { + * q = "John Mayer Trio" + * num = "50" + * } + **/ +- (NSDictionary *)parseGetParams +{ + if(![request isHeaderComplete]) return nil; + + NSDictionary *result = nil; + + NSURL *url = [request url]; + if(url) + { + NSString *query = [url query]; + if (query) + { + result = [self parseParams:query]; + } + } + + return result; +} + +/** + * Attempts to parse the given range header into a series of sequential non-overlapping ranges. + * If successfull, the variables 'ranges' and 'rangeIndex' will be updated, and YES will be returned. + * Otherwise, NO is returned, and the range request should be ignored. + **/ +- (BOOL)parseRangeRequest:(NSString *)rangeHeader withContentLength:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Examples of byte-ranges-specifier values (assuming an entity-body of length 10000): + // + // - The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 + // + // - The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999 + // + // - The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 + // + // - Or bytes=9500- + // + // - The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 + // + // - Several legal but not canonical specifications of the second 500 bytes (byte offsets 500-999, inclusive): + // bytes=500-600,601-999 + // bytes=500-700,601-999 + // + + NSRange eqsignRange = [rangeHeader rangeOfString:@"="]; + + if(eqsignRange.location == NSNotFound) return NO; + + NSUInteger tIndex = eqsignRange.location; + NSUInteger fIndex = eqsignRange.location + eqsignRange.length; + + NSMutableString *rangeType = [[rangeHeader substringToIndex:tIndex] mutableCopy]; + NSMutableString *rangeValue = [[rangeHeader substringFromIndex:fIndex] mutableCopy]; + + CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeType); + CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeValue); + + if([rangeType caseInsensitiveCompare:@"bytes"] != NSOrderedSame) return NO; + + NSArray *rangeComponents = [rangeValue componentsSeparatedByString:@","]; + + if([rangeComponents count] == 0) return NO; + + ranges = [[NSMutableArray alloc] initWithCapacity:[rangeComponents count]]; + + rangeIndex = 0; + + // Note: We store all range values in the form of DDRange structs, wrapped in NSValue objects. + // Since DDRange consists of UInt64 values, the range extends up to 16 exabytes. + + NSUInteger i; + for (i = 0; i < [rangeComponents count]; i++) + { + NSString *rangeComponent = [rangeComponents objectAtIndex:i]; + + NSRange dashRange = [rangeComponent rangeOfString:@"-"]; + + if (dashRange.location == NSNotFound) + { + // We're dealing with an individual byte number + + UInt64 byteIndex; + if(![NSNumber parseString:rangeComponent intoUInt64:&byteIndex]) return NO; + + if(byteIndex >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(byteIndex, 1)]]; + } + else + { + // We're dealing with a range of bytes + + tIndex = dashRange.location; + fIndex = dashRange.location + dashRange.length; + + NSString *r1str = [rangeComponent substringToIndex:tIndex]; + NSString *r2str = [rangeComponent substringFromIndex:fIndex]; + + UInt64 r1, r2; + + BOOL hasR1 = [NSNumber parseString:r1str intoUInt64:&r1]; + BOOL hasR2 = [NSNumber parseString:r2str intoUInt64:&r2]; + + if (!hasR1) + { + // We're dealing with a "-[#]" range + // + // r2 is the number of ending bytes to include in the range + + if(!hasR2) return NO; + if(r2 > contentLength) return NO; + + UInt64 startIndex = contentLength - r2; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(startIndex, r2)]]; + } + else if (!hasR2) + { + // We're dealing with a "[#]-" range + // + // r1 is the starting index of the range, which goes all the way to the end + + if(r1 >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, contentLength - r1)]]; + } + else + { + // We're dealing with a normal "[#]-[#]" range + // + // Note: The range is inclusive. So 0-1 has a length of 2 bytes. + + if(r1 > r2) return NO; + if(r2 >= contentLength) return NO; + + [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, r2 - r1 + 1)]]; + } + } + } + + if([ranges count] == 0) return NO; + + // Now make sure none of the ranges overlap + + for (i = 0; i < [ranges count] - 1; i++) + { + DDRange range1 = [[ranges objectAtIndex:i] ddrangeValue]; + + NSUInteger j; + for (j = i+1; j < [ranges count]; j++) + { + DDRange range2 = [[ranges objectAtIndex:j] ddrangeValue]; + + DDRange iRange = DDIntersectionRange(range1, range2); + + if(iRange.length != 0) + { + return NO; + } + } + } + + // Sort the ranges + + [ranges sortUsingSelector:@selector(ddrangeCompare:)]; + + return YES; +} + +- (NSString *)requestURI +{ + if(request == nil) return nil; + + return [[request url] relativeString]; +} + +/** + * This method is called after a full HTTP request has been received. + * The current request is in the HTTPMessage request variable. + **/ +- (void)replyToHTTPRequest +{ + HTTPLogTrace(); + + if (HTTP_LOG_VERBOSE) + { + NSData *tempData = [request messageData]; + + NSString *tempStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding]; + HTTPLogVerbose(@"%@[%p]: Received HTTP request:\n%@", THIS_FILE, self, tempStr); + } + + // Check the HTTP version + // We only support version 1.0 and 1.1 + + NSString *version = [request version]; + if (![version isEqualToString:HTTPVersion1_1] && ![version isEqualToString:HTTPVersion1_0]) + { + [self handleVersionNotSupported:version]; + return; + } + + // Extract requested URI + NSString *uri = [self requestURI]; + + // Extract the method + NSString *method = [request method]; + + // Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag: + + // Respond properly to HTTP 'GET' and 'HEAD' commands + httpResponse = [self httpResponseForMethod:method URI:uri]; + + if (httpResponse == nil) + { + [self handleResourceNotFound]; + return; + } + + [self sendResponseHeadersAndBody]; +} + +/** + * Prepares a single-range response. + * + * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. + **/ +- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Status Code 206 - Partial Content + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; + + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", range.length]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + + NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; + NSString *contentRangeStr = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; + [response setHeaderField:@"Content-Range" value:contentRangeStr]; + + return response; +} + +/** + * Prepares a multi-range response. + * + * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. + **/ +- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength +{ + HTTPLogTrace(); + + // Status Code 206 - Partial Content + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; + + // We have to send each range using multipart/byteranges + // So each byterange has to be prefix'd and suffix'd with the boundry + // Example: + // + // HTTP/1.1 206 Partial Content + // Content-Length: 220 + // Content-Type: multipart/byteranges; boundary=4554d24e986f76dd6 + // + // + // --4554d24e986f76dd6 + // Content-Range: bytes 0-25/4025 + // + // [...] + // --4554d24e986f76dd6 + // Content-Range: bytes 3975-4024/4025 + // + // [...] + // --4554d24e986f76dd6-- + + ranges_headers = [[NSMutableArray alloc] initWithCapacity:[ranges count]]; + + CFUUIDRef theUUID = CFUUIDCreate(NULL); + ranges_boundry = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + + NSString *startingBoundryStr = [NSString stringWithFormat:@"\r\n--%@\r\n", ranges_boundry]; + NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; + + UInt64 actualContentLength = 0; + + NSUInteger i; + for (i = 0; i < [ranges count]; i++) + { + DDRange range = [[ranges objectAtIndex:i] ddrangeValue]; + + NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; + NSString *contentRangeVal = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; + NSString *contentRangeStr = [NSString stringWithFormat:@"Content-Range: %@\r\n\r\n", contentRangeVal]; + + NSString *fullHeader = [startingBoundryStr stringByAppendingString:contentRangeStr]; + NSData *fullHeaderData = [fullHeader dataUsingEncoding:NSUTF8StringEncoding]; + + [ranges_headers addObject:fullHeaderData]; + + actualContentLength += [fullHeaderData length]; + actualContentLength += range.length; + } + + NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; + + actualContentLength += [endingBoundryData length]; + + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", actualContentLength]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + + NSString *contentTypeStr = [NSString stringWithFormat:@"multipart/byteranges; boundary=%@", ranges_boundry]; + [response setHeaderField:@"Content-Type" value:contentTypeStr]; + + return response; +} + +/** + * Returns the chunk size line that must precede each chunk of data when using chunked transfer encoding. + * This consists of the size of the data, in hexadecimal, followed by a CRLF. + **/ +- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length +{ + return [[NSString stringWithFormat:@"%lx\r\n", (unsigned long)length] dataUsingEncoding:NSUTF8StringEncoding]; +} + +/** + * Returns the data that signals the end of a chunked transfer. + **/ +- (NSData *)chunkedTransferFooter +{ + // Each data chunk is preceded by a size line (in hex and including a CRLF), + // followed by the data itself, followed by another CRLF. + // After every data chunk has been sent, a zero size line is sent, + // followed by optional footer (which are just more headers), + // and followed by a CRLF on a line by itself. + + return [@"\r\n0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (void)sendResponseHeadersAndBody +{ + if ([httpResponse respondsToSelector:@selector(delayResponseHeaders)]) + { + if ([httpResponse delayResponseHeaders]) + { + return; + } + } + + BOOL isChunked = NO; + + if ([httpResponse respondsToSelector:@selector(isChunked)]) + { + isChunked = [httpResponse isChunked]; + } + + // If a response is "chunked", this simply means the HTTPResponse object + // doesn't know the content-length in advance. + + UInt64 contentLength = 0; + + if (!isChunked) + { + contentLength = [httpResponse contentLength]; + } + + // Check for specific range request + NSString *rangeHeader = [request headerField:@"Range"]; + + BOOL isRangeRequest = NO; + + // If the response is "chunked" then we don't know the exact content-length. + // This means we'll be unable to process any range requests. + // This is because range requests might include a range like "give me the last 100 bytes" + + if (!isChunked && rangeHeader) + { + if ([self parseRangeRequest:rangeHeader withContentLength:contentLength]) + { + isRangeRequest = YES; + } + } + + HTTPMessage *response; + + if (!isRangeRequest) + { + // Create response + // Default status code: 200 - OK + NSInteger status = 200; + + if ([httpResponse respondsToSelector:@selector(status)]) + { + status = [httpResponse status]; + } + response = [[HTTPMessage alloc] initResponseWithStatusCode:status description:nil version:HTTPVersion1_1]; + + if (isChunked) + { + [response setHeaderField:@"Transfer-Encoding" value:@"chunked"]; + } + else + { + NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", contentLength]; + [response setHeaderField:@"Content-Length" value:contentLengthStr]; + } + } + else + { + if ([ranges count] == 1) + { + response = [self newUniRangeResponse:contentLength]; + } + else + { + response = [self newMultiRangeResponse:contentLength]; + } + } + + BOOL isZeroLengthResponse = !isChunked && (contentLength == 0); + + // If they issue a 'HEAD' command, we don't have to include the file + // If they issue a 'GET' command, we need to include the file + + if ([[request method] isEqualToString:@"HEAD"] || isZeroLengthResponse) + { + NSData *responseData = [self preprocessResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + + sentResponseHeaders = YES; + } + else + { + // Write the header response + NSData *responseData = [self preprocessResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + sentResponseHeaders = YES; + + // Now we need to send the body of the response + if (!isRangeRequest) + { + // Regular request + NSData *data = [httpResponse readDataOfLength:READ_CHUNKSIZE]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + if (isChunked) + { + NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; + [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; + + if ([httpResponse isDone]) + { + NSData *footer = [self chunkedTransferFooter]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + else + { + NSData *footer = [GCDAsyncSocket CRLFData]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; + } + } + else + { + long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } + } + else + { + // Client specified a byte range in request + + if ([ranges count] == 1) + { + // Client is requesting a single range + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + long tag = [data length] == range.length ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } + else + { + // Client is requesting multiple ranges + // We have to send each range using multipart/byteranges + + // Write range header + NSData *rangeHeaderData = [ranges_headers objectAtIndex:0]; + [asyncSocket writeData:rangeHeaderData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + // Start writing range body + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + } + } + +} + +/** + * Returns the number of bytes of the http response body that are sitting in asyncSocket's write queue. + * + * We keep track of this information in order to keep our memory footprint low while + * working with asynchronous HTTPResponse objects. + **/ +- (NSUInteger)writeQueueSize +{ + NSUInteger result = 0; + + NSUInteger i; + for(i = 0; i < [responseDataSizes count]; i++) + { + result += [[responseDataSizes objectAtIndex:i] unsignedIntegerValue]; + } + + return result; +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for standard (non-range) responses. + **/ +- (void)continueSendingStandardResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous HTTPResponse object informs us that it has more available data for us to send. + // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSData *data = [httpResponse readDataOfLength:available]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + BOOL isChunked = NO; + + if ([httpResponse respondsToSelector:@selector(isChunked)]) + { + isChunked = [httpResponse isChunked]; + } + + if (isChunked) + { + NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; + [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; + + if([httpResponse isDone]) + { + NSData *footer = [self chunkedTransferFooter]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + else + { + NSData *footer = [GCDAsyncSocket CRLFData]; + [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; + } + } + else + { + long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for single-range responses. + **/ +- (void)continueSendingSingleRangeResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous response informs us that is has more available data for us to send. + // In the case of the asynchronous response, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; + + UInt64 offset = [httpResponse offset]; + UInt64 bytesRead = offset - range.location; + UInt64 bytesLeft = range.length - bytesRead; + + if (bytesLeft > 0) + { + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + long tag = [data length] == bytesLeft ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; + } + } +} + +/** + * Sends more data, if needed, without growing the write queue over its approximate size limit. + * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. + * + * This method should only be called for multi-range responses. + **/ +- (void)continueSendingMultiRangeResponseBody +{ + HTTPLogTrace(); + + // This method is called when either asyncSocket has finished writing one of the response data chunks, + // or when an asynchronous HTTPResponse object informs us that is has more available data for us to send. + // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, + // and shove it onto asyncSocket's write queue. + // Doing so could negatively affect the memory footprint of the application. + // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. + // + // Note that this does not affect the rate at which the HTTPResponse object may generate data. + // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. + // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely + // use the calls to readDataOfLength as an indication to start generating more data. + // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate + // at which the socket is able to send it. + + NSUInteger writeQueueSize = [self writeQueueSize]; + + if(writeQueueSize >= READ_CHUNKSIZE) return; + + DDRange range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; + + UInt64 offset = [httpResponse offset]; + UInt64 bytesRead = offset - range.location; + UInt64 bytesLeft = range.length - bytesRead; + + if (bytesLeft > 0) + { + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + else + { + if (++rangeIndex < [ranges count]) + { + // Write range header + NSData *rangeHeader = [ranges_headers objectAtIndex:rangeIndex]; + [asyncSocket writeData:rangeHeader withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; + + // Start writing range body + range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; + + [httpResponse setOffset:range.location]; + + NSUInteger available = READ_CHUNKSIZE - writeQueueSize; + NSUInteger bytesToRead = range.length < available ? (NSUInteger)range.length : available; + + NSData *data = [httpResponse readDataOfLength:bytesToRead]; + + if ([data length] > 0) + { + [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; + + [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; + } + } + else + { + // We're not done yet - we still have to send the closing boundry tag + NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; + NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; + + [asyncSocket writeData:endingBoundryData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Responses +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns an array of possible index pages. + * For example: {"index.html", "index.htm"} + **/ +- (NSArray *)directoryIndexFileNames +{ + HTTPLogTrace(); + + // Override me to support other index pages. + + return [NSArray arrayWithObjects:@"index.html", @"index.htm", nil]; +} + +- (NSString *)filePathForURI:(NSString *)path +{ + return [self filePathForURI:path allowDirectory:NO]; +} + +/** + * Converts relative URI path into full file-system path. + **/ +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory +{ + HTTPLogTrace(); + + // Override me to perform custom path mapping. + // For example you may want to use a default file other than index.html, or perhaps support multiple types. + + NSString *documentRoot = [config documentRoot]; + + // Part 0: Validate document root setting. + // + // If there is no configured documentRoot, + // then it makes no sense to try to return anything. + + if (documentRoot == nil) + { + HTTPLogWarn(@"%@[%p]: No configured document root", THIS_FILE, self); + return nil; + } + + // Part 1: Strip parameters from the url + // + // E.g.: /page.html?q=22&var=abc -> /page.html + + NSURL *docRoot = [NSURL fileURLWithPath:documentRoot isDirectory:YES]; + if (docRoot == nil) + { + HTTPLogWarn(@"%@[%p]: Document root is invalid file path", THIS_FILE, self); + return nil; + } + + NSString *relativePath = [[NSURL URLWithString:path relativeToURL:docRoot] relativePath]; + + // Part 2: Append relative path to document root (base path) + // + // E.g.: relativePath="/images/icon.png" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites/images/icon.png" + // + // We also standardize the path. + // + // E.g.: "Users/robbie/Sites/images/../index.html" -> "/Users/robbie/Sites/index.html" + + NSString *fullPath = [[documentRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath]; + + if ([relativePath isEqualToString:@"/"]) + { + fullPath = [fullPath stringByAppendingString:@"/"]; + } + + // Part 3: Prevent serving files outside the document root. + // + // Sneaky requests may include ".." in the path. + // + // E.g.: relativePath="../Documents/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Documents/TopSecret.doc" + // + // E.g.: relativePath="../Sites_Secret/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites_Secret/TopSecret" + + if (![documentRoot hasSuffix:@"/"]) + { + documentRoot = [documentRoot stringByAppendingString:@"/"]; + } + + if (![fullPath hasPrefix:documentRoot]) + { + HTTPLogWarn(@"%@[%p]: Request for file outside document root", THIS_FILE, self); + return nil; + } + + // Part 4: Search for index page if path is pointing to a directory + if (!allowDirectory) + { + BOOL isDir = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) + { + NSArray *indexFileNames = [self directoryIndexFileNames]; + + for (NSString *indexFileName in indexFileNames) + { + NSString *indexFilePath = [fullPath stringByAppendingPathComponent:indexFileName]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:indexFilePath isDirectory:&isDir] && !isDir) + { + return indexFilePath; + } + } + + // No matching index files found in directory + return nil; + } + } + + return fullPath; +} + +/** + * This method is called to get a response for a request. + * You may return any object that adopts the HTTPResponse protocol. + * The HTTPServer comes with two such classes: HTTPFileResponse and HTTPDataResponse. + * HTTPFileResponse is a wrapper for an NSFileHandle object, and is the preferred way to send a file response. + * HTTPDataResponse is a wrapper for an NSData object, and may be used to send a custom response. + **/ +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to provide custom responses. + + return nil; +} + +- (WebSocket *)webSocketForURI:(NSString *)path +{ + HTTPLogTrace(); + + // Override me to provide custom WebSocket responses. + // To do so, simply override the base WebSocket implementation, and add your custom functionality. + // Then return an instance of your custom WebSocket here. + // + // For example: + // + // if ([path isEqualToString:@"/myAwesomeWebSocketStream"]) + // { + // return [[[MyWebSocket alloc] initWithRequest:request socket:asyncSocket] autorelease]; + // } + // + // return [super webSocketForURI:path]; + + return nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Uploads +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after receiving all HTTP headers, but before reading any of the request body. + **/ +- (void)prepareForBodyWithSize:(UInt64)contentLength +{ + // Override me to allocate buffers, file handles, etc. +} + +/** + * This method is called to handle data read from a POST / PUT. + * The given data is part of the request body. + **/ +- (void)processBodyData:(NSData *)postDataChunk +{ + // Override me to do something useful with a POST / PUT. + // If the post is small, such as a simple form, you may want to simply append the data to the request. + // If the post is big, such as a file upload, you may want to store the file to disk. + // + // Remember: In order to support LARGE POST uploads, the data is read in chunks. + // This prevents a 50 MB upload from being stored in RAM. + // The size of the chunks are limited by the POST_CHUNKSIZE definition. + // Therefore, this method may be called multiple times for the same POST request. +} + +/** + * This method is called after the request body has been fully read but before the HTTP request is processed. + **/ +- (void)finishBody +{ + // Override me to perform any final operations on an upload. + // For example, if you were saving the upload to disk this would be + // the hook to flush any pending data to disk and maybe close the file. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called if the HTML version is other than what is supported + **/ +- (void)handleVersionNotSupported:(NSString *)version +{ + // Override me for custom error handling of unsupported http version responses + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogWarn(@"HTTP Server: Error 505 - Version Not Supported: %@ (%@)", version, [self requestURI]); + + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:505 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; + +} + +/** + * Called if we receive some sort of malformed HTTP request. + * The data parameter is the invalid HTTP header line, including CRLF, as read from GCDAsyncSocket. + * The data parameter may also be nil if the request as a whole was invalid, such as a POST with no Content-Length. + **/ +- (void)handleInvalidRequest:(NSData *)data +{ + // Override me for custom error handling of invalid HTTP requests + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogWarn(@"HTTP Server: Error 400 - Bad Request (%@)", [self requestURI]); + + // Status Code 400 - Bad Request + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:400 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + [response setHeaderField:@"Connection" value:@"close"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; + + + // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. + // We do this because we couldn't parse the request, + // so we won't be able to recover and move on to another request afterwards. + // In other words, we wouldn't know where the first request ends and the second request begins. +} + +/** + * Called if we receive a HTTP request with a method other than GET or HEAD. + **/ +- (void)handleUnknownMethod:(NSString *)method +{ + // Override me for custom error handling of 405 method not allowed responses. + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + // + // See also: supportsMethod:atPath: + + HTTPLogWarn(@"HTTP Server: Error 405 - Method Not Allowed: %@ (%@)", method, [self requestURI]); + + // Status code 405 - Method Not Allowed + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:405 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + [response setHeaderField:@"Connection" value:@"close"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; + + + // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. + // We do this because the method may include an http body. + // Since we can't be sure, we should close the connection. +} + +/** + * Called if we're unable to find the requested resource. + **/ +- (void)handleResourceNotFound +{ + // Override me for custom error handling of 404 not found responses + // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. + // You can also use preprocessErrorResponse: to add an optional HTML body. + + HTTPLogInfo(@"HTTP Server: Error 404 - Not Found (%@)", [self requestURI]); + + // Status Code 404 - Not Found + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:404 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Headers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Gets the current date and time, formatted properly (according to RFC) for insertion into an HTTP header. + **/ +- (NSString *)dateAsString:(NSDate *)date +{ + // From Apple's Documentation (Data Formatting Guide -> Date Formatters -> Cache Formatters for Efficiency): + // + // "Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, + // it is typically more efficient to cache a single instance than to create and dispose of multiple instances. + // One approach is to use a static variable." + // + // This was discovered to be true in massive form via issue #46: + // + // "Was doing some performance benchmarking using instruments and httperf. Using this single optimization + // I got a 26% speed improvement - from 1000req/sec to 3800req/sec. Not insignificant. + // The culprit? Why, NSDateFormatter, of course!" + // + // Thus, we are using a static NSDateFormatter here. + + static NSDateFormatter *df; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // Example: Sun, 06 Nov 1994 08:49:37 GMT + + df = [[NSDateFormatter alloc] init]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; + [df setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; + [df setDateFormat:@"EEE, dd MMM y HH:mm:ss 'GMT'"]; + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]]; + + // For some reason, using zzz in the format string produces GMT+00:00 + }); + + return [df stringFromDate:date]; +} + +/** + * This method is called immediately prior to sending the response headers. + * This method adds standard header fields, and then converts the response to an NSData object. + **/ +- (NSData *)preprocessResponse:(HTTPMessage *)response +{ + HTTPLogTrace(); + + // Override me to customize the response headers + // You'll likely want to add your own custom headers, and then return [super preprocessResponse:response] + + // Add standard headers + NSString *now = [self dateAsString:[NSDate date]]; + [response setHeaderField:@"Date" value:now]; + + // Add server capability headers + [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; + + // Add optional response headers + if ([httpResponse respondsToSelector:@selector(httpHeaders)]) + { + NSDictionary *responseHeaders = [httpResponse httpHeaders]; + + NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; + NSString *key; + + while ((key = [keyEnumerator nextObject])) + { + NSString *value = [responseHeaders objectForKey:key]; + + [response setHeaderField:key value:value]; + } + } + + return [response messageData]; +} + +/** + * This method is called immediately prior to sending the response headers (for an error). + * This method adds standard header fields, and then converts the response to an NSData object. + **/ +- (NSData *)preprocessErrorResponse:(HTTPMessage *)response +{ + HTTPLogTrace(); + + // Override me to customize the error response headers + // You'll likely want to add your own custom headers, and then return [super preprocessErrorResponse:response] + // + // Notes: + // You can use [response statusCode] to get the type of error. + // You can use [response setBody:data] to add an optional HTML body. + // If you add a body, don't forget to update the Content-Length. + // + // if ([response statusCode] == 404) + // { + // NSString *msg = @"Error 404 - Not Found"; + // NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; + // + // [response setBody:msgData]; + // + // NSString *contentLengthStr = [NSString stringWithFormat:@"%lu", (unsigned long)[msgData length]]; + // [response setHeaderField:@"Content-Length" value:contentLengthStr]; + // } + + // Add standard headers + NSString *now = [self dateAsString:[NSDate date]]; + [response setHeaderField:@"Date" value:now]; + + // Add server capability headers + [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; + + // Add optional response headers + if ([httpResponse respondsToSelector:@selector(httpHeaders)]) + { + NSDictionary *responseHeaders = [httpResponse httpHeaders]; + + NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; + NSString *key; + + while((key = [keyEnumerator nextObject])) + { + NSString *value = [responseHeaders objectForKey:key]; + + [response setHeaderField:key value:value]; + } + } + + return [response messageData]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark GCDAsyncSocket Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after the socket has successfully read data from the stream. + * Remember that this method will only be called after the socket reaches a CRLF, or after it's read the proper length. + **/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)tag +{ + if (tag == HTTP_REQUEST_HEADER) + { + // Append the header line to the http message + BOOL result = [request appendData:data]; + if (!result) + { + HTTPLogWarn(@"%@[%p]: Malformed request", THIS_FILE, self); + + [self handleInvalidRequest:data]; + } + else if (![request isHeaderComplete]) + { + // We don't have a complete header yet + // That is, we haven't yet received a CRLF on a line by itself, indicating the end of the header + if (++numHeaderLines > MAX_HEADER_LINES) + { + // Reached the maximum amount of header lines in a single HTTP request + // This could be an attempted DOS attack + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnect + return; + } + else + { + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_SUBSEQUENT_HEADER_LINE + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_HEADER]; + } + } + else + { + // We have an entire HTTP request header from the client + + // Extract the method (such as GET, HEAD, POST, etc) + NSString *method = [request method]; + + // Extract the uri (such as "/index.html") + NSString *uri = [self requestURI]; + + // Check for a Transfer-Encoding field + NSString *transferEncoding = [request headerField:@"Transfer-Encoding"]; + + // Check for a Content-Length field + NSString *contentLength = [request headerField:@"Content-Length"]; + + // Content-Length MUST be present for upload methods (such as POST or PUT) + // and MUST NOT be present for other methods. + BOOL expectsUpload = [self expectsRequestBodyFromMethod:method atPath:uri]; + + if (expectsUpload) + { + if (transferEncoding && ![transferEncoding caseInsensitiveCompare:@"Chunked"]) + { + requestContentLength = -1; + } + else + { + if (contentLength == nil) + { + HTTPLogWarn(@"%@[%p]: Method expects request body, but had no specified Content-Length", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) + { + HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + } + } + else + { + if (contentLength != nil) + { + // Received Content-Length header for method not expecting an upload. + // This better be zero... + + if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) + { + HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (requestContentLength > 0) + { + HTTPLogWarn(@"%@[%p]: Method not expecting request body had non-zero Content-Length", + THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + } + + requestContentLength = 0; + requestContentLengthReceived = 0; + } + + // Check to make sure the given method is supported + if (![self supportsMethod:method atPath:uri]) + { + // The method is unsupported - either in general, or for this specific request + // Send a 405 - Method not allowed response + [self handleUnknownMethod:method]; + return; + } + + if (expectsUpload) + { + // Reset the total amount of data received for the upload + requestContentLengthReceived = 0; + + // Prepare for the upload + [self prepareForBodyWithSize:requestContentLength]; + + if (requestContentLength > 0) + { + // Start reading the request body + if (requestContentLength == -1) + { + // Chunked transfer + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_CHUNK_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_SIZE]; + } + else + { + NSUInteger bytesToRead; + if (requestContentLength < POST_CHUNKSIZE) + bytesToRead = (NSUInteger)requestContentLength; + else + bytesToRead = POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_BODY]; + } + } + else + { + // Empty upload + [self finishBody]; + [self replyToHTTPRequest]; + } + } + else + { + // Now we need to reply to the request + [self replyToHTTPRequest]; + } + } + } + else + { + BOOL doneReadingRequest = NO; + + // A chunked message body contains a series of chunks, + // followed by a line with "0" (zero), + // followed by optional footers (just like headers), + // and a blank line. + // + // Each chunk consists of two parts: + // + // 1. A line with the size of the chunk data, in hex, + // possibly followed by a semicolon and extra parameters you can ignore (none are currently standard), + // and ending with CRLF. + // 2. The data itself, followed by CRLF. + // + // Part 1 is represented by HTTP_REQUEST_CHUNK_SIZE + // Part 2 is represented by HTTP_REQUEST_CHUNK_DATA and HTTP_REQUEST_CHUNK_TRAILER + // where the trailer is the CRLF that follows the data. + // + // The optional footers and blank line are represented by HTTP_REQUEST_CHUNK_FOOTER. + + if (tag == HTTP_REQUEST_CHUNK_SIZE) + { + // We have just read in a line with the size of the chunk data, in hex, + // possibly followed by a semicolon and extra parameters that can be ignored, + // and ending with CRLF. + + NSString *sizeLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + errno = 0; // Reset errno before calling strtoull() to ensure it is always zero on success + requestChunkSize = (UInt64)strtoull([sizeLine UTF8String], NULL, 16); + requestChunkSizeReceived = 0; + + if (errno != 0) + { + HTTPLogWarn(@"%@[%p]: Method expects chunk size, but received something else", THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + if (requestChunkSize > 0) + { + NSUInteger bytesToRead; + bytesToRead = (requestChunkSize < POST_CHUNKSIZE) ? (NSUInteger)requestChunkSize : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_DATA]; + } + else + { + // This is the "0" (zero) line, + // which is to be followed by optional footers (just like headers) and finally a blank line. + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_FOOTER]; + } + + return; + } + else if (tag == HTTP_REQUEST_CHUNK_DATA) + { + // We just read part of the actual data. + + requestContentLengthReceived += [data length]; + requestChunkSizeReceived += [data length]; + + [self processBodyData:data]; + + UInt64 bytesLeft = requestChunkSize - requestChunkSizeReceived; + if (bytesLeft > 0) + { + NSUInteger bytesToRead = (bytesLeft < POST_CHUNKSIZE) ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_DATA]; + } + else + { + // We've read in all the data for this chunk. + // The data is followed by a CRLF, which we need to read (and basically ignore) + + [asyncSocket readDataToLength:2 + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_CHUNK_TRAILER]; + } + + return; + } + else if (tag == HTTP_REQUEST_CHUNK_TRAILER) + { + // This should be the CRLF following the data. + // Just ensure it's a CRLF. + + if (![data isEqualToData:[GCDAsyncSocket CRLFData]]) + { + HTTPLogWarn(@"%@[%p]: Method expects chunk trailer, but is missing", THIS_FILE, self); + + [self handleInvalidRequest:nil]; + return; + } + + // Now continue with the next chunk + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_CHUNK_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_SIZE]; + + } + else if (tag == HTTP_REQUEST_CHUNK_FOOTER) + { + if (++numHeaderLines > MAX_HEADER_LINES) + { + // Reached the maximum amount of header lines in a single HTTP request + // This could be an attempted DOS attack + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnect + return; + } + + if ([data length] > 2) + { + // We read in a footer. + // In the future we may want to append these to the request. + // For now we ignore, and continue reading the footers, waiting for the final blank line. + + [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] + withTimeout:TIMEOUT_READ_BODY + maxLength:MAX_HEADER_LINE_LENGTH + tag:HTTP_REQUEST_CHUNK_FOOTER]; + } + else + { + doneReadingRequest = YES; + } + } + else // HTTP_REQUEST_BODY + { + // Handle a chunk of data from the POST body + + requestContentLengthReceived += [data length]; + [self processBodyData:data]; + + if (requestContentLengthReceived < requestContentLength) + { + // We're not done reading the post body yet... + + UInt64 bytesLeft = requestContentLength - requestContentLengthReceived; + + NSUInteger bytesToRead = bytesLeft < POST_CHUNKSIZE ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; + + [asyncSocket readDataToLength:bytesToRead + withTimeout:TIMEOUT_READ_BODY + tag:HTTP_REQUEST_BODY]; + } + else + { + doneReadingRequest = YES; + } + } + + // Now that the entire body has been received, we need to reply to the request + + if (doneReadingRequest) + { + [self finishBody]; + [self replyToHTTPRequest]; + } + } +} + +/** + * This method is called after the socket has successfully written data to the stream. + **/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag +{ + BOOL doneSendingResponse = NO; + + if (tag == HTTP_PARTIAL_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + + // We only wrote a part of the response - there may be more + [self continueSendingStandardResponseBody]; + } + else if (tag == HTTP_CHUNKED_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue. + // This will allow asynchronous responses to continue sending more data. + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // Don't continue sending the response yet. + // The chunked footer that was sent after the body will tell us if we have more data to send. + } + else if (tag == HTTP_CHUNKED_RESPONSE_FOOTER) + { + // Normal chunked footer indicating we have more data to send (non final footer). + [self continueSendingStandardResponseBody]; + } + else if (tag == HTTP_PARTIAL_RANGE_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // We only wrote a part of the range - there may be more + [self continueSendingSingleRangeResponseBody]; + } + else if (tag == HTTP_PARTIAL_RANGES_RESPONSE_BODY) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) { + [responseDataSizes removeObjectAtIndex:0]; + } + // We only wrote part of the range - there may be more, or there may be more ranges + [self continueSendingMultiRangeResponseBody]; + } + else if (tag == HTTP_RESPONSE || tag == HTTP_FINAL_RESPONSE) + { + // Update the amount of data we have in asyncSocket's write queue + if ([responseDataSizes count] > 0) + { + [responseDataSizes removeObjectAtIndex:0]; + } + + doneSendingResponse = YES; + } + + if (doneSendingResponse) + { + // Inform the http response that we're done + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } + + + if (tag == HTTP_FINAL_RESPONSE) + { + // Cleanup after the last request + [self finishResponse]; + + // Terminate the connection + [asyncSocket disconnect]; + + // Explictly return to ensure we don't do anything after the socket disconnects + return; + } + else + { + if ([self shouldDie]) + { + // Cleanup after the last request + // Note: Don't do this before calling shouldDie, as it needs the request object still. + [self finishResponse]; + + // The only time we should invoke [self die] is from socketDidDisconnect, + // or if the socket gets taken over by someone else like a WebSocket. + + [asyncSocket disconnect]; + } + else + { + // Cleanup after the last request + [self finishResponse]; + + // Prepare for the next request + + // If this assertion fails, it likely means you overrode the + // finishBody method and forgot to call [super finishBody]. + NSAssert(request == nil, @"Request not properly released in finishBody"); + + request = [[HTTPMessage alloc] initEmptyRequest]; + + numHeaderLines = 0; + sentResponseHeaders = NO; + + // And start listening for more requests + [self startReadingRequest]; + } + } + } +} + +/** + * Sent after the socket has been disconnected. + **/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + HTTPLogTrace(); + + asyncSocket = nil; + + [self die]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark HTTPResponse Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method may be called by asynchronous HTTPResponse objects. + * That is, HTTPResponse objects that return YES in their "- (BOOL)isAsynchronous" method. + * + * This informs us that the response object has generated more data that we may be able to send. + **/ +- (void)responseHasAvailableData:(NSObject *)sender +{ + HTTPLogTrace(); + + // We always dispatch this asynchronously onto our connectionQueue, + // even if the connectionQueue is the current queue. + // + // We do this to give the HTTPResponse classes the flexibility to call + // this method whenever they want, even from within a readDataOfLength method. + + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (sender != httpResponse) + { + HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); + return; + } + + if (!sentResponseHeaders) + { + [self sendResponseHeadersAndBody]; + } + else + { + if (ranges == nil) + { + [self continueSendingStandardResponseBody]; + } + else + { + if ([ranges count] == 1) + [self continueSendingSingleRangeResponseBody]; + else + [self continueSendingMultiRangeResponseBody]; + } + } + }}); +} + +/** + * This method is called if the response encounters some critical error, + * and it will be unable to fullfill the request. + **/ +- (void)responseDidAbort:(NSObject *)sender +{ + HTTPLogTrace(); + + // We always dispatch this asynchronously onto our connectionQueue, + // even if the connectionQueue is the current queue. + // + // We do this to give the HTTPResponse classes the flexibility to call + // this method whenever they want, even from within a readDataOfLength method. + + dispatch_async(connectionQueue, ^{ @autoreleasepool { + + if (sender != httpResponse) + { + HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); + return; + } + + [asyncSocket disconnectAfterWriting]; + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Post Request +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called after each response has been fully sent. + * Since a single connection may handle multiple request/responses, this method may be called multiple times. + * That is, it will be called after completion of each response. + **/ +- (void)finishResponse +{ + HTTPLogTrace(); + + // Override me if you want to perform any custom actions after a response has been fully sent. + // This is the place to release memory or resources associated with the last request. + // + // If you override this method, you should take care to invoke [super finishResponse] at some point. + + request = nil; + + httpResponse = nil; + + ranges = nil; + ranges_headers = nil; + ranges_boundry = nil; +} + +/** + * This method is called after each successful response has been fully sent. + * It determines whether the connection should stay open and handle another request. + **/ +- (BOOL)shouldDie +{ + HTTPLogTrace(); + + // Override me if you have any need to force close the connection. + // You may do so by simply returning YES. + // + // If you override this method, you should take care to fall through with [super shouldDie] + // instead of returning NO. + + + BOOL shouldDie = NO; + + NSString *version = [request version]; + if ([version isEqualToString:HTTPVersion1_1]) + { + // HTTP version 1.1 + // Connection should only be closed if request included "Connection: close" header + + NSString *connection = [request headerField:@"Connection"]; + + shouldDie = (connection && ([connection caseInsensitiveCompare:@"close"] == NSOrderedSame)); + } + else if ([version isEqualToString:HTTPVersion1_0]) + { + // HTTP version 1.0 + // Connection should be closed unless request included "Connection: Keep-Alive" header + + NSString *connection = [request headerField:@"Connection"]; + + if (connection == nil) + shouldDie = YES; + else + shouldDie = [connection caseInsensitiveCompare:@"Keep-Alive"] != NSOrderedSame; + } + + return shouldDie; +} + +- (void)die +{ + HTTPLogTrace(); + + // Override me if you want to perform any custom actions when a connection is closed. + // Then call [super die] when you're done. + // + // See also the finishResponse method. + // + // Important: There is a rare timing condition where this method might get invoked twice. + // If you override this method, you should be prepared for this situation. + + // Inform the http response that we're done + if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) + { + [httpResponse connectionDidClose]; + } + + // Release the http response so we don't call it's connectionDidClose method again in our dealloc method + httpResponse = nil; + + // Post notification of dead connection + // This will allow our server to release us from its array of connections + [[NSNotificationCenter defaultCenter] postNotificationName:HTTPConnectionDidDieNotification object:self]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPConfig + +@synthesize server; +@synthesize documentRoot; +@synthesize queue; + +- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot +{ + if ((self = [super init])) + { + server = aServer; + documentRoot = aDocumentRoot; + } + return self; +} + +- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot queue:(dispatch_queue_t)q +{ + if ((self = [super init])) + { + server = aServer; + + documentRoot = [aDocumentRoot stringByStandardizingPath]; + if ([documentRoot hasSuffix:@"/"]) + { + documentRoot = [documentRoot stringByAppendingString:@"/"]; + } + + if (q) + { + queue = q; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(queue); +#endif + } + } + return self; +} + +- (void)dealloc +{ +#if !OS_OBJECT_USE_OBJC + if (queue) dispatch_release(queue); +#endif +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h new file mode 100644 index 0000000..84ee8da --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h @@ -0,0 +1,122 @@ +/** + * In order to provide fast and flexible logging, this project uses Cocoa Lumberjack. + * + * The Google Code page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * Here's what you need to know concerning how logging is setup for CocoaHTTPServer: + * + * There are 4 log levels: + * - Error + * - Warning + * - Info + * - Verbose + * + * In addition to this, there is a Trace flag that can be enabled. + * When tracing is enabled, it spits out the methods that are being called. + * + * Please note that tracing is separate from the log levels. + * For example, one could set the log level to warning, and enable tracing. + * + * All logging is asynchronous, except errors. + * To use logging within your own custom files, follow the steps below. + * + * Step 1: + * Import this header in your implementation file: + * + * #import "HTTPLogging.h" + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const int httpLogLevel = HTTP_LOG_LEVEL_VERBOSE; + * + * If you wish to enable tracing, you could do something like this: + * + * // Debug levels: off, error, warn, info, verbose + * static const int httpLogLevel = HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_TRACE; + * + * Step 3: + * Replace your NSLog statements with HTTPLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> HTTPLogError(@"Fatal error, no dohickey found!"); + * + * HTTPLog works exactly the same as NSLog. + * This means you can pass it multiple variables just like NSLog. + **/ + +// Define logging context for every log message coming from the HTTP server. +// The logging context can be extracted from the DDLogMessage from within the logging framework, +// which gives loggers, formatters, and filters the ability to optionally process them differently. + +#define HTTP_LOG_CONTEXT 80 + +// Configure log levels. + +#define HTTP_LOG_FLAG_ERROR (1 << 0) // 0...00001 +#define HTTP_LOG_FLAG_WARN (1 << 1) // 0...00010 +#define HTTP_LOG_FLAG_INFO (1 << 2) // 0...00100 +#define HTTP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000 + +#define HTTP_LOG_LEVEL_OFF 0 // 0...00000 +#define HTTP_LOG_LEVEL_ERROR (HTTP_LOG_LEVEL_OFF | HTTP_LOG_FLAG_ERROR) // 0...00001 +#define HTTP_LOG_LEVEL_WARN (HTTP_LOG_LEVEL_ERROR | HTTP_LOG_FLAG_WARN) // 0...00011 +#define HTTP_LOG_LEVEL_INFO (HTTP_LOG_LEVEL_WARN | HTTP_LOG_FLAG_INFO) // 0...00111 +#define HTTP_LOG_LEVEL_VERBOSE (HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_VERBOSE) // 0...01111 + +// Setup fine grained logging. +// The first 4 bits are being used by the standard log levels (0 - 3) +// +// We're going to add tracing, but NOT as a log level. +// Tracing can be turned on and off independently of log level. + +#define HTTP_LOG_FLAG_TRACE (1 << 4) // 0...10000 + +// Setup the usual boolean macros. + +#define HTTP_LOG_ERROR (httpLogLevel & HTTP_LOG_FLAG_ERROR) +#define HTTP_LOG_WARN (httpLogLevel & HTTP_LOG_FLAG_WARN) +#define HTTP_LOG_INFO (httpLogLevel & HTTP_LOG_FLAG_INFO) +#define HTTP_LOG_VERBOSE (httpLogLevel & HTTP_LOG_FLAG_VERBOSE) +#define HTTP_LOG_TRACE (httpLogLevel & HTTP_LOG_FLAG_TRACE) + +// Configure asynchronous logging. +// We follow the default configuration, +// but we reserve a special macro to easily disable asynchronous logging for debugging purposes. + +#define HTTP_LOG_ASYNC_ENABLED YES + +#define HTTP_LOG_ASYNC_ERROR ( NO && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_WARN (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_INFO (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_VERBOSE (YES && HTTP_LOG_ASYNC_ENABLED) +#define HTTP_LOG_ASYNC_TRACE (YES && HTTP_LOG_ASYNC_ENABLED) + +// Define logging primitives. + +#define HTTPLogError(...) { } + +#define HTTPLogWarn(...) { } + +#define HTTPLogInfo(...) { } + +#define HTTPLogVerbose(...) { } + +#define HTTPLogTrace() { } + +#define HTTPLogTrace2(...) { } + + +#define HTTPLogCError(...) { } + +#define HTTPLogCWarn(...) { } + +#define HTTPLogCInfo(...) { } + +#define HTTPLogCVerbose(...) { } + +#define HTTPLogCTrace() { } + +#define HTTPLogCTrace2(...) { } + diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h new file mode 100644 index 0000000..04e0bd2 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h @@ -0,0 +1,48 @@ +/** + * The HTTPMessage class is a simple Objective-C wrapper around Apple's CFHTTPMessage class. + **/ + +#import + +#if TARGET_OS_IPHONE +// Note: You may need to add the CFNetwork Framework to your project +#import +#endif + +#define HTTPVersion1_0 ((NSString *)kCFHTTPVersion1_0) +#define HTTPVersion1_1 ((NSString *)kCFHTTPVersion1_1) + + +@interface HTTPMessage : NSObject +{ + CFHTTPMessageRef message; +} + +- (id)initEmptyRequest; + +- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version; + +- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version; + +- (BOOL)appendData:(NSData *)data; + +- (BOOL)isHeaderComplete; + +- (NSString *)version; + +- (NSString *)method; +- (NSURL *)url; + +- (NSInteger)statusCode; + +- (NSDictionary *)allHeaderFields; +- (NSString *)headerField:(NSString *)headerField; + +- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue; + +- (NSData *)messageData; + +- (NSData *)body; +- (void)setBody:(NSData *)body; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m new file mode 100644 index 0000000..345d847 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m @@ -0,0 +1,114 @@ +#import "HTTPMessage.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +@implementation HTTPMessage + +- (id)initEmptyRequest +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateEmpty(NULL, YES); + } + return self; +} + +- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateRequest(NULL, + (__bridge CFStringRef)method, + (__bridge CFURLRef)url, + (__bridge CFStringRef)version); + } + return self; +} + +- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version +{ + if ((self = [super init])) + { + message = CFHTTPMessageCreateResponse(NULL, + (CFIndex)code, + (__bridge CFStringRef)description, + (__bridge CFStringRef)version); + } + return self; +} + +- (void)dealloc +{ + if (message) + { + CFRelease(message); + } +} + +- (BOOL)appendData:(NSData *)data +{ + return CFHTTPMessageAppendBytes(message, [data bytes], [data length]); +} + +- (BOOL)isHeaderComplete +{ + return CFHTTPMessageIsHeaderComplete(message); +} + +- (NSString *)version +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message); +} + +- (NSString *)method +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message); +} + +- (NSURL *)url +{ + return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message); +} + +- (NSInteger)statusCode +{ + return (NSInteger)CFHTTPMessageGetResponseStatusCode(message); +} + +- (NSDictionary *)allHeaderFields +{ + return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message); +} + +- (NSString *)headerField:(NSString *)headerField +{ + return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)headerField); +} + +- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue +{ + CFHTTPMessageSetHeaderFieldValue(message, + (__bridge CFStringRef)headerField, + (__bridge CFStringRef)headerFieldValue); +} + +- (NSData *)messageData +{ + return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message); +} + +- (NSData *)body +{ + return (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message); +} + +- (void)setBody:(NSData *)body +{ + CFHTTPMessageSetBody(message, (__bridge CFDataRef)body); +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h new file mode 100644 index 0000000..726ca5d --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h @@ -0,0 +1,149 @@ +#import + + +@protocol HTTPResponse + +/** + * Returns the length of the data in bytes. + * If you don't know the length in advance, implement the isChunked method and have it return YES. + **/ +- (UInt64)contentLength; + +/** + * The HTTP server supports range requests in order to allow things like + * file download resumption and optimized streaming on mobile devices. + **/ +- (UInt64)offset; +- (void)setOffset:(UInt64)offset; + +/** + * Returns the data for the response. + * You do not have to return data of the exact length that is given. + * You may optionally return data of a lesser length. + * However, you must never return data of a greater length than requested. + * Doing so could disrupt proper support for range requests. + * + * To support asynchronous responses, read the discussion at the bottom of this header. + **/ +- (NSData *)readDataOfLength:(NSUInteger)length; + +/** + * Should only return YES after the HTTPConnection has read all available data. + * That is, all data for the response has been returned to the HTTPConnection via the readDataOfLength method. + **/ +- (BOOL)isDone; + +@optional + +/** + * If you need time to calculate any part of the HTTP response headers (status code or header fields), + * this method allows you to delay sending the headers so that you may asynchronously execute the calculations. + * Simply implement this method and return YES until you have everything you need concerning the headers. + * + * This method ties into the asynchronous response architecture of the HTTPConnection. + * You should read the full discussion at the bottom of this header. + * + * If you return YES from this method, + * the HTTPConnection will wait for you to invoke the responseHasAvailableData method. + * After you do, the HTTPConnection will again invoke this method to see if the response is ready to send the headers. + * + * You should only delay sending the headers until you have everything you need concerning just the headers. + * Asynchronously generating the body of the response is not an excuse to delay sending the headers. + * Instead you should tie into the asynchronous response architecture, and use techniques such as the isChunked method. + * + * Important: You should read the discussion at the bottom of this header. + **/ +- (BOOL)delayResponseHeaders; + +/** + * Status code for response. + * Allows for responses such as redirect (301), etc. + **/ +- (NSInteger)status; + +/** + * If you want to add any extra HTTP headers to the response, + * simply return them in a dictionary in this method. + **/ +- (NSDictionary *)httpHeaders; + +/** + * If you don't know the content-length in advance, + * implement this method in your custom response class and return YES. + * + * Important: You should read the discussion at the bottom of this header. + **/ +- (BOOL)isChunked; + +/** + * This method is called from the HTTPConnection class when the connection is closed, + * or when the connection is finished with the response. + * If your response is asynchronous, you should implement this method so you know not to + * invoke any methods on the HTTPConnection after this method is called (as the connection may be deallocated). + **/ +- (void)connectionDidClose; + +@end + + +/** + * Important notice to those implementing custom asynchronous and/or chunked responses: + * + * HTTPConnection supports asynchronous responses. All you have to do in your custom response class is + * asynchronously generate the response, and invoke HTTPConnection's responseHasAvailableData method. + * You don't have to wait until you have all of the response ready to invoke this method. For example, if you + * generate the response in incremental chunks, you could call responseHasAvailableData after generating + * each chunk. Please see the HTTPAsyncFileResponse class for an example of how to do this. + * + * The normal flow of events for an HTTPConnection while responding to a request is like this: + * - Send http resopnse headers + * - Get data from response via readDataOfLength method. + * - Add data to asyncSocket's write queue. + * - Wait for asyncSocket to notify it that the data has been sent. + * - Get more data from response via readDataOfLength method. + * - ... continue this cycle until the entire response has been sent. + * + * With an asynchronous response, the flow is a little different. + * + * First the HTTPResponse is given the opportunity to postpone sending the HTTP response headers. + * This allows the response to asynchronously execute any code needed to calculate a part of the header. + * An example might be the response needs to generate some custom header fields, + * or perhaps the response needs to look for a resource on network-attached storage. + * Since the network-attached storage may be slow, the response doesn't know whether to send a 200 or 404 yet. + * In situations such as this, the HTTPResponse simply implements the delayResponseHeaders method and returns YES. + * After returning YES from this method, the HTTPConnection will wait until the response invokes its + * responseHasAvailableData method. After this occurs, the HTTPConnection will again query the delayResponseHeaders + * method to see if the response is ready to send the headers. + * This cycle will continue until the delayResponseHeaders method returns NO. + * + * You should only delay sending the response headers until you have everything you need concerning just the headers. + * Asynchronously generating the body of the response is not an excuse to delay sending the headers. + * + * After the response headers have been sent, the HTTPConnection calls your readDataOfLength method. + * You may or may not have any available data at this point. If you don't, then simply return nil. + * You should later invoke HTTPConnection's responseHasAvailableData when you have data to send. + * + * You don't have to keep track of when you return nil in the readDataOfLength method, or how many times you've invoked + * responseHasAvailableData. Just simply call responseHasAvailableData whenever you've generated new data, and + * return nil in your readDataOfLength whenever you don't have any available data in the requested range. + * HTTPConnection will automatically detect when it should be requesting new data and will act appropriately. + * + * It's important that you also keep in mind that the HTTP server supports range requests. + * The setOffset method is mandatory, and should not be ignored. + * Make sure you take into account the offset within the readDataOfLength method. + * You should also be aware that the HTTPConnection automatically sorts any range requests. + * So if your setOffset method is called with a value of 100, then you can safely release bytes 0-99. + * + * HTTPConnection can also help you keep your memory footprint small. + * Imagine you're dynamically generating a 10 MB response. You probably don't want to load all this data into + * RAM, and sit around waiting for HTTPConnection to slowly send it out over the network. All you need to do + * is pay attention to when HTTPConnection requests more data via readDataOfLength. This is because HTTPConnection + * will never allow asyncSocket's write queue to get much bigger than READ_CHUNKSIZE bytes. You should + * consider how you might be able to take advantage of this fact to generate your asynchronous response on demand, + * while at the same time keeping your memory footprint small, and your application lightning fast. + * + * If you don't know the content-length in advanced, you should also implement the isChunked method. + * This means the response will not include a Content-Length header, and will instead use "Transfer-Encoding: chunked". + * There's a good chance that if your response is asynchronous and dynamic, it's also chunked. + * If your response is chunked, you don't need to worry about range requests. + **/ diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h new file mode 100644 index 0000000..6934321 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h @@ -0,0 +1,126 @@ +#import + +@class GCDAsyncSocket; +@class WebSocket; + +#if TARGET_OS_IPHONE +#define IMPLEMENTED_PROTOCOLS +#else +#define IMPLEMENTED_PROTOCOLS +#endif + + +@interface HTTPServer : NSObject IMPLEMENTED_PROTOCOLS +{ + // Underlying asynchronous TCP/IP socket + GCDAsyncSocket *asyncSocket; + + // Dispatch queues + dispatch_queue_t serverQueue; + dispatch_queue_t connectionQueue; + void *IsOnServerQueueKey; + void *IsOnConnectionQueueKey; + + // HTTP server configuration + NSString *documentRoot; + Class connectionClass; + NSString *interface; + UInt16 port; + + // Connection management + NSMutableArray *connections; + NSLock *connectionsLock; + + BOOL isRunning; +} + +/** + * Specifies the document root to serve files from. + * For example, if you set this to "/Users//Sites", + * then it will serve files out of the local Sites directory (including subdirectories). + * + * The default value is nil. + * The default server configuration will not serve any files until this is set. + * + * If you change the documentRoot while the server is running, + * the change will affect future incoming http connections. + **/ +- (NSString *)documentRoot; +- (void)setDocumentRoot:(NSString *)value; + +/** + * The connection class is the class used to handle incoming HTTP connections. + * + * The default value is [HTTPConnection class]. + * You can override HTTPConnection, and then set this to [MyHTTPConnection class]. + * + * If you change the connectionClass while the server is running, + * the change will affect future incoming http connections. + **/ +- (Class)connectionClass; +- (void)setConnectionClass:(Class)value; + +/** + * Set what interface you'd like the server to listen on. + * By default this is nil, which causes the server to listen on all available interfaces like en1, wifi etc. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + **/ +- (NSString *)interface; +- (void)setInterface:(NSString *)value; + +/** + * The port number to run the HTTP server on. + * + * The default port number is zero, meaning the server will automatically use any available port. + * This is the recommended port value, as it avoids possible port conflicts with other applications. + * Technologies such as Bonjour can be used to allow other applications to automatically discover the port number. + * + * Note: As is common on most OS's, you need root privledges to bind to port numbers below 1024. + * + * You can change the port property while the server is running, but it won't affect the running server. + * To actually change the port the server is listening for connections on you'll need to restart the server. + * + * The listeningPort method will always return the port number the running server is listening for connections on. + * If the server is not running this method returns 0. + **/ +- (UInt16)port; +- (UInt16)listeningPort; +- (void)setPort:(UInt16)value; + +/** + * Attempts to starts the server on the configured port, interface, etc. + * + * If an error occurs, this method returns NO and sets the errPtr (if given). + * Otherwise returns YES on success. + * + * Some examples of errors that might occur: + * - You specified the server listen on a port which is already in use by another application. + * - You specified the server listen on a port number below 1024, which requires root priviledges. + * + * Code Example: + * + * NSError *err = nil; + * if (![httpServer start:&err]) + * { + * NSLog(@"Error starting http server: %@", err); + * } + **/ +- (BOOL)start:(NSError **)errPtr; + +/** + * Stops the server, preventing it from accepting any new connections. + * You may specify whether or not you want to close the existing client connections. + * + * The default stop method (with no arguments) will close any existing connections. (It invokes [self stop:NO]) + **/ +- (void)stop; +- (void)stop:(BOOL)keepExistingConnections; + +- (BOOL)isRunning; + +- (NSUInteger)numberOfHTTPConnections; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m new file mode 100644 index 0000000..4a0ca7f --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m @@ -0,0 +1,372 @@ +#import "HTTPServer.h" +#import "HTTPConnection.h" +#import "HTTPLogging.h" + +#import "GCDAsyncSocket.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Wimplicit-retain-self" +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" +#pragma clang diagnostic ignored "-Wunused" + +// Log levels: off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation HTTPServer + +/** + * Standard Constructor. + * Instantiates an HTTP server, but does not start it. + **/ +- (id)init +{ + if ((self = [super init])) + { + HTTPLogTrace(); + + // Setup underlying dispatch queues + serverQueue = dispatch_queue_create("HTTPServer", NULL); + connectionQueue = dispatch_queue_create("HTTPConnection", NULL); + + IsOnServerQueueKey = &IsOnServerQueueKey; + IsOnConnectionQueueKey = &IsOnConnectionQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; // Whatever, just not null + + dispatch_queue_set_specific(serverQueue, IsOnServerQueueKey, nonNullUnusedPointer, NULL); + dispatch_queue_set_specific(connectionQueue, IsOnConnectionQueueKey, nonNullUnusedPointer, NULL); + + // Initialize underlying GCD based tcp socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:(id)self delegateQueue:serverQueue]; + + // Use default connection class of HTTPConnection + connectionClass = [HTTPConnection self]; + + // By default bind on all available interfaces, en1, wifi etc + interface = nil; + + // Use a default port of 0 + // This will allow the kernel to automatically pick an open port for us + port = 0; + + // Initialize arrays to hold all the HTTP connections + connections = [[NSMutableArray alloc] init]; + + connectionsLock = [[NSLock alloc] init]; + + // Register for notifications of closed connections + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(connectionDidDie:) + name:HTTPConnectionDidDieNotification + object:nil]; + + isRunning = NO; + } + return self; +} + +/** + * Standard Deconstructor. + * Stops the server, and clients, and releases any resources connected with this instance. + **/ +- (void)dealloc +{ + HTTPLogTrace(); + + // Remove notification observer + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // Stop the server if it's running + [self stop]; + + // Release all instance variables + +#if !OS_OBJECT_USE_OBJC + dispatch_release(serverQueue); + dispatch_release(connectionQueue); +#endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The document root is filesystem root for the webserver. + * Thus requests for /index.html will be referencing the index.html file within the document root directory. + * All file requests are relative to this document root. + **/ +- (NSString *)documentRoot +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = documentRoot; + }); + + return result; +} + +- (void)setDocumentRoot:(NSString *)value +{ + HTTPLogTrace(); + + // Document root used to be of type NSURL. + // Add type checking for early warning to developers upgrading from older versions. + + if (value && ![value isKindOfClass:[NSString class]]) + { + HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter", + THIS_FILE, THIS_METHOD, NSStringFromClass([value class])); + return; + } + + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + documentRoot = valueCopy; + }); + +} + +/** + * The connection class is the class that will be used to handle connections. + * That is, when a new connection is created, an instance of this class will be intialized. + * The default connection class is HTTPConnection. + * If you use a different connection class, it is assumed that the class extends HTTPConnection + **/ +- (Class)connectionClass +{ + __block Class result; + + dispatch_sync(serverQueue, ^{ + result = connectionClass; + }); + + return result; +} + +- (void)setConnectionClass:(Class)value +{ + HTTPLogTrace(); + + dispatch_async(serverQueue, ^{ + connectionClass = value; + }); +} + +/** + * What interface to bind the listening socket to. + **/ +- (NSString *)interface +{ + __block NSString *result; + + dispatch_sync(serverQueue, ^{ + result = interface; + }); + + return result; +} + +- (void)setInterface:(NSString *)value +{ + NSString *valueCopy = [value copy]; + + dispatch_async(serverQueue, ^{ + interface = valueCopy; + }); + +} + +/** + * The port to listen for connections on. + * By default this port is initially set to zero, which allows the kernel to pick an available port for us. + * After the HTTP server has started, the port being used may be obtained by this method. + **/ +- (UInt16)port +{ + __block UInt16 result; + + dispatch_sync(serverQueue, ^{ + result = port; + }); + + return result; +} + +- (UInt16)listeningPort +{ + __block UInt16 result; + + dispatch_sync(serverQueue, ^{ + if (isRunning) + result = [asyncSocket localPort]; + else + result = 0; + }); + + return result; +} + +- (void)setPort:(UInt16)value +{ + HTTPLogTrace(); + + dispatch_async(serverQueue, ^{ + port = value; + }); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Control +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)start:(NSError **)errPtr +{ + HTTPLogTrace(); + + __block BOOL success = YES; + __block NSError *err = nil; + + dispatch_sync(serverQueue, ^{ @autoreleasepool { + + success = [asyncSocket acceptOnInterface:interface port:port error:&err]; + if (success) + { + HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]); + + isRunning = YES; + } + else + { + HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err); + } + }}); + + if (errPtr) + *errPtr = err; + + return success; +} + +- (void)stop +{ + [self stop:NO]; +} + +- (void)stop:(BOOL)keepExistingConnections +{ + HTTPLogTrace(); + + dispatch_sync(serverQueue, ^{ @autoreleasepool { + // Stop listening / accepting incoming connections + [asyncSocket disconnect]; + isRunning = NO; + + if (!keepExistingConnections) + { + // Stop all HTTP connections the server owns + [connectionsLock lock]; + for (HTTPConnection *connection in connections) + { + [connection stop]; + } + [connections removeAllObjects]; + [connectionsLock unlock]; + } + }}); +} + +- (BOOL)isRunning +{ + __block BOOL result; + + dispatch_sync(serverQueue, ^{ + result = isRunning; + }); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Status +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the number of http client connections that are currently connected to the server. + **/ +- (NSUInteger)numberOfHTTPConnections +{ + NSUInteger result = 0; + + [connectionsLock lock]; + result = [connections count]; + [connectionsLock unlock]; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Incoming Connections +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (HTTPConfig *)config +{ + // Override me if you want to provide a custom config to the new connection. + // + // Generally this involves overriding the HTTPConfig class to include any custom settings, + // and then having this method return an instance of 'MyHTTPConfig'. + + // Note: Think you can make the server faster by putting each connection on its own queue? + // Then benchmark it before and after and discover for yourself the shocking truth! + // + // Try the apache benchmark tool (already installed on your Mac): + // $ ab -n 1000 -c 1 http://localhost:/some_path.html + + return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:connectionQueue]; +} + +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket +{ + HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket + configuration:[self config]]; + [connectionsLock lock]; + [connections addObject:newConnection]; + [connectionsLock unlock]; + + [newConnection start]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted. + * It allows us to remove the connection from our array. + **/ +- (void)connectionDidDie:(NSNotification *)notification +{ + // Note: This method is called on the connection queue that posted the notification + + [connectionsLock lock]; + + HTTPLogTrace(); + [connections removeObject:[notification object]]; + + [connectionsLock unlock]; +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE b/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE new file mode 100644 index 0000000..64c3c90 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE @@ -0,0 +1,18 @@ +Software License Agreement (BSD License) + +Copyright (c) 2011, Deusty, LLC +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Neither the name of Deusty nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Deusty, LLC. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h new file mode 100644 index 0000000..309a6d9 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h @@ -0,0 +1,13 @@ +#import +#import "HTTPResponse.h" + + +@interface HTTPDataResponse : NSObject +{ + NSUInteger offset; + NSData *data; +} + +- (id)initWithData:(NSData *)data; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m new file mode 100644 index 0000000..79c5bca --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m @@ -0,0 +1,83 @@ +#import "HTTPDataResponse.h" +#import "HTTPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Wcast-qual" +#pragma clang diagnostic ignored "-Wunused-variable" + +// Log levels : off, error, warn, info, verbose +// Other flags: trace +static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE; + + +@implementation HTTPDataResponse + +- (id)initWithData:(NSData *)dataParam +{ + if((self = [super init])) + { + HTTPLogTrace(); + + offset = 0; + data = dataParam; + } + return self; +} + +- (void)dealloc +{ + HTTPLogTrace(); + +} + +- (UInt64)contentLength +{ + UInt64 result = (UInt64)[data length]; + + HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, result); + + return result; +} + +- (UInt64)offset +{ + HTTPLogTrace(); + + return offset; +} + +- (void)setOffset:(UInt64)offsetParam +{ + HTTPLogTrace2(@"%@[%p]: setOffset:%lu", THIS_FILE, self, (unsigned long)offset); + + offset = (NSUInteger)offsetParam; +} + +- (NSData *)readDataOfLength:(NSUInteger)lengthParameter +{ + HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)lengthParameter); + + NSUInteger remaining = [data length] - offset; + NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining; + + void *bytes = (void *)(((char*)[data bytes]) + offset); + + offset += length; + + return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO]; +} + +- (BOOL)isDone +{ + BOOL result = (offset == [data length]); + + HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); + + return result; +} + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h new file mode 100644 index 0000000..0b4fed9 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h @@ -0,0 +1,9 @@ +#import "HTTPResponse.h" + +@interface HTTPErrorResponse : NSObject { + NSInteger _status; +} + +- (id)initWithErrorCode:(int)httpErrorCode; + +@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m new file mode 100644 index 0000000..3309cf1 --- /dev/null +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m @@ -0,0 +1,40 @@ +#import "HTTPErrorResponse.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +@implementation HTTPErrorResponse + +-(id)initWithErrorCode:(int)httpErrorCode +{ + if ((self = [super init])) + { + _status = httpErrorCode; + } + + return self; +} + +- (UInt64) contentLength { + return 0; +} + +- (UInt64) offset { + return 0; +} + +- (void)setOffset:(UInt64)offset { + ; +} + +- (NSData*) readDataOfLength:(NSUInteger)length { + return nil; +} + +- (BOOL) isDone { + return YES; +} + +- (NSInteger) status { + return _status; +} +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h new file mode 100644 index 0000000..e3930fc --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h @@ -0,0 +1,13 @@ +#import +#import "HTTPResponse.h" + +// Wraps an HTTPResponse object to allow setting a custom status code +// without needing to create subclasses of every response. +@interface HTTPResponseProxy : NSObject + +@property (nonatomic) NSObject *response; +@property (nonatomic) NSInteger status; + +- (NSInteger)customStatus; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m new file mode 100644 index 0000000..f74f3ad --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m @@ -0,0 +1,84 @@ +#import "HTTPResponseProxy.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +@implementation HTTPResponseProxy + +@synthesize response; +@synthesize status; + +- (NSInteger)status { + if (status != 0) { + return status; + } else if ([response respondsToSelector:@selector(status)]) { + return [response status]; + } + + return 200; +} + +- (void)setStatus:(NSInteger)statusCode { + status = statusCode; +} + +- (NSInteger)customStatus { + return status; +} + +// Implement the required HTTPResponse methods +- (UInt64)contentLength { + if (response) { + return [response contentLength]; + } else { + return 0; + } +} + +- (UInt64)offset { + if (response) { + return [response offset]; + } else { + return 0; + } +} + +- (void)setOffset:(UInt64)offset { + if (response) { + [response setOffset:offset]; + } +} + +- (NSData *)readDataOfLength:(NSUInteger)length { + if (response) { + return [response readDataOfLength:length]; + } else { + return nil; + } +} + +- (BOOL)isDone { + if (response) { + return [response isDone]; + } else { + return YES; + } +} + +// Forward all other invocations to the actual response object +- (void)forwardInvocation:(NSInvocation *)invocation { + if ([response respondsToSelector:[invocation selector]]) { + [invocation invokeWithTarget:response]; + } else { + [super forwardInvocation:invocation]; + } +} + +- (BOOL)respondsToSelector:(SEL)selector { + if ([super respondsToSelector:selector]) + return YES; + + return [response respondsToSelector:selector]; +} + +@end + diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE b/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE new file mode 100644 index 0000000..717caf7 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Matt Stevens + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h new file mode 100644 index 0000000..185a2b7 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h @@ -0,0 +1,18 @@ +#import +#import "RoutingHTTPServer.h" + +@interface Route : NSObject + +@property (nonatomic) NSRegularExpression *regex; +@property (nonatomic, copy) RequestHandler handler; + +#if __has_feature(objc_arc_weak) +@property (nonatomic, weak) id target; +#else +@property (nonatomic, assign) id target; +#endif + +@property (nonatomic, assign) SEL selector; +@property (nonatomic) NSArray *keys; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m new file mode 100644 index 0000000..8c9e7e5 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m @@ -0,0 +1,11 @@ +#import "Route.h" + +@implementation Route + +@synthesize regex; +@synthesize handler; +@synthesize target; +@synthesize selector; +@synthesize keys; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h new file mode 100644 index 0000000..0219add --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h @@ -0,0 +1,16 @@ +#import +@class HTTPMessage; + +@interface RouteRequest : NSObject + +@property (nonatomic, readonly) NSDictionary *headers; +@property (nonatomic, readonly) NSDictionary *params; + +- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)params; +- (NSString *)header:(NSString *)field; +- (id)param:(NSString *)name; +- (NSString *)method; +- (NSURL *)url; +- (NSData *)body; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m new file mode 100644 index 0000000..50046d0 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m @@ -0,0 +1,50 @@ +#import "RouteRequest.h" +#import "HTTPMessage.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Widiomatic-parentheses" + +@implementation RouteRequest { + HTTPMessage *message; +} + +@synthesize params; + +- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)parameters { + if (self = [super init]) { + params = parameters; + message = msg; + } + return self; +} + +- (NSDictionary *)headers { + return [message allHeaderFields]; +} + +- (NSString *)header:(NSString *)field { + return [message headerField:field]; +} + +- (id)param:(NSString *)name { + return [params objectForKey:name]; +} + +- (NSString *)method { + return [message method]; +} + +- (NSURL *)url { + return [message url]; +} + +- (NSData *)body { + return [message body]; +} + +- (NSString *)description { + NSData *data = [message messageData]; + return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; +} + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h new file mode 100644 index 0000000..688002f --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h @@ -0,0 +1,20 @@ +#import +#import "HTTPResponse.h" +@class HTTPConnection; +@class HTTPResponseProxy; + +@interface RouteResponse : NSObject + +@property (nonatomic, unsafe_unretained, readonly) HTTPConnection *connection; +@property (nonatomic, readonly) NSDictionary *headers; +@property (nonatomic, strong) NSObject *response; +@property (nonatomic, readonly) NSObject *proxiedResponse; +@property (nonatomic) NSInteger statusCode; + +- (id)initWithConnection:(HTTPConnection *)theConnection; +- (void)setHeader:(NSString *)field value:(NSString *)value; +- (void)respondWithString:(NSString *)string; +- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding; +- (void)respondWithData:(NSData *)data; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m new file mode 100644 index 0000000..47db7b6 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m @@ -0,0 +1,66 @@ +#import "RouteResponse.h" +#import "HTTPConnection.h" +#import "HTTPDataResponse.h" +#import "HTTPResponseProxy.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Widiomatic-parentheses" + +@implementation RouteResponse { + NSMutableDictionary *headers; + HTTPResponseProxy *proxy; +} + +@synthesize connection; +@synthesize headers; + +- (id)initWithConnection:(HTTPConnection *)theConnection { + if (self = [super init]) { + connection = theConnection; + headers = [[NSMutableDictionary alloc] init]; + proxy = [[HTTPResponseProxy alloc] init]; + } + return self; +} + +- (NSObject *)response { + return proxy.response; +} + +- (void)setResponse:(NSObject *)response { + proxy.response = response; +} + +- (NSObject *)proxiedResponse { + if (proxy.response != nil || proxy.customStatus != 0 || [headers count] > 0) { + return proxy; + } + + return nil; +} + +- (NSInteger)statusCode { + return proxy.status; +} + +- (void)setStatusCode:(NSInteger)status { + proxy.status = status; +} + +- (void)setHeader:(NSString *)field value:(NSString *)value { + [headers setObject:value forKey:field]; +} + +- (void)respondWithString:(NSString *)string { + [self respondWithString:string encoding:NSUTF8StringEncoding]; +} + +- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding { + [self respondWithData:[string dataUsingEncoding:encoding]]; +} + +- (void)respondWithData:(NSData *)data { + self.response = [[HTTPDataResponse alloc] initWithData:data]; +} + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h new file mode 100644 index 0000000..1f6cd27 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h @@ -0,0 +1,5 @@ +#import +#import "HTTPConnection.h" + +@interface RoutingConnection : HTTPConnection +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m new file mode 100644 index 0000000..3eee192 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m @@ -0,0 +1,142 @@ +#import "RoutingConnection.h" +#import "RoutingHTTPServer.h" +#import "HTTPMessage.h" +#import "HTTPResponseProxy.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Widiomatic-parentheses" +#pragma clang diagnostic ignored "-Wundeclared-selector" + +@implementation RoutingConnection { + __unsafe_unretained RoutingHTTPServer *http; + NSDictionary *headers; +} + +- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig { + if (self = [super initWithAsyncSocket:newSocket configuration:aConfig]) { + NSAssert([config.server isKindOfClass:[RoutingHTTPServer class]], + @"A RoutingConnection is being used with a server that is not a %@", + NSStringFromClass([RoutingHTTPServer class])); + + http = (RoutingHTTPServer *)config.server; + } + return self; +} + +- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path { + + if ([http supportsMethod:method]) + return YES; + + return [super supportsMethod:method atPath:path]; +} + +- (BOOL)shouldHandleRequestForMethod:(NSString *)method atPath:(NSString *)path { + // The default implementation is strict about the use of Content-Length. Either + // a given method + path combination must *always* include data or *never* + // include data. The routing connection is lenient, a POST that sometimes does + // not include data or a GET that sometimes does is fine. It is up to the route + // implementations to decide how to handle these situations. + return YES; +} + +- (void)processBodyData:(NSData *)postDataChunk { + BOOL result = [request appendData:postDataChunk]; + if (!result) { + // TODO: Log + } +} + +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path { + NSURL *url = [request url]; + NSString *query = nil; + NSDictionary *params = [NSDictionary dictionary]; + headers = nil; + + if (url) { + path = [url path]; // Strip the query string from the path + query = [url query]; + if (query) { + params = [self parseParams:query]; + } + } + + RouteResponse *response = [http routeMethod:method withPath:path parameters:params request:request connection:self]; + if (response != nil) { + headers = response.headers; + return response.proxiedResponse; + } + + // Set a MIME type for static files if possible + NSObject *staticResponse = [super httpResponseForMethod:method URI:path]; + if (staticResponse && [staticResponse respondsToSelector:@selector(filePath)]) { + NSString *mimeType = [http mimeTypeForPath:[staticResponse performSelector:@selector(filePath)]]; + if (mimeType) { + headers = [NSDictionary dictionaryWithObject:mimeType forKey:@"Content-Type"]; + } + } + return staticResponse; +} + +- (void)responseHasAvailableData:(NSObject *)sender { + HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse; + if (proxy.response == sender) { + [super responseHasAvailableData:httpResponse]; + } +} + +- (void)responseDidAbort:(NSObject *)sender { + HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse; + if (proxy.response == sender) { + [super responseDidAbort:httpResponse]; + } +} + +- (void)setHeadersForResponse:(HTTPMessage *)response isError:(BOOL)isError { + [http.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { + [response setHeaderField:field value:value]; + }]; + + if (headers && !isError) { + [headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { + [response setHeaderField:field value:value]; + }]; + } + + // Set the connection header if not already specified + NSString *connection = [response headerField:@"Connection"]; + if (!connection) { + connection = [self shouldDie] ? @"close" : @"keep-alive"; + [response setHeaderField:@"Connection" value:connection]; + } +} + +- (NSData *)preprocessResponse:(HTTPMessage *)response { + [self setHeadersForResponse:response isError:NO]; + return [super preprocessResponse:response]; +} + +- (NSData *)preprocessErrorResponse:(HTTPMessage *)response { + [self setHeadersForResponse:response isError:YES]; + return [super preprocessErrorResponse:response]; +} + +- (BOOL)shouldDie { + __block BOOL shouldDie = [super shouldDie]; + + // Allow custom headers to determine if the connection should be closed + if (!shouldDie && headers) { + [headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { + if ([field caseInsensitiveCompare:@"connection"] == NSOrderedSame) { + if ([value caseInsensitiveCompare:@"close"] == NSOrderedSame) { + shouldDie = YES; + } + *stop = YES; + } + }]; + } + + return shouldDie; +} + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h new file mode 100644 index 0000000..91c7768 --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h @@ -0,0 +1,55 @@ +#import + +//! Project version number for Peertalk. +FOUNDATION_EXPORT double RoutingHTTPServerVersionNumber; + +//! Project version string for Peertalk. +FOUNDATION_EXPORT const unsigned char RoutingHTTPServerVersionString[]; + +#import "HTTPServer.h" +#import "HTTPConnection.h" +#import "HTTPResponse.h" +#import "RouteResponse.h" +#import "RouteRequest.h" +#import "RoutingConnection.h" + +#import "GCDAsyncSocket.h" + +typedef void (^RequestHandler)(RouteRequest *request, RouteResponse *response); + +@interface RoutingHTTPServer : HTTPServer + +@property (nonatomic, readonly) NSDictionary *defaultHeaders; + +// Specifies headers that will be set on every response. +// These headers can be overridden by RouteResponses. +- (void)setDefaultHeaders:(NSDictionary *)headers; +- (void)setDefaultHeader:(NSString *)field value:(NSString *)value; + +// Returns the dispatch queue on which routes are processed. +// By default this is NULL and routes are processed on CocoaHTTPServer's +// connection queue. You can specify a queue to process routes on, such as +// dispatch_get_main_queue() to process all routes on the main thread. +- (dispatch_queue_t)routeQueue; +- (void)setRouteQueue:(dispatch_queue_t)queue; + +- (NSDictionary *)mimeTypes; +- (void)setMIMETypes:(NSDictionary *)types; +- (void)setMIMEType:(NSString *)type forExtension:(NSString *)ext; +- (NSString *)mimeTypeForPath:(NSString *)path; + +// Convenience methods. Yes I know, this is Cocoa and we don't use convenience +// methods because typing lengthy primitives over and over and over again is +// elegant with the beauty and the poetry. These are just, you know, here. +- (void)get:(NSString *)path withBlock:(RequestHandler)block; +- (void)post:(NSString *)path withBlock:(RequestHandler)block; +- (void)put:(NSString *)path withBlock:(RequestHandler)block; +- (void)delete:(NSString *)path withBlock:(RequestHandler)block; + +- (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block; +- (void)handleMethod:(NSString *)method withPath:(NSString *)path target:(id)target selector:(SEL)selector; + +- (BOOL)supportsMethod:(NSString *)method; +- (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)request connection:(HTTPConnection *)connection; + +@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m new file mode 100644 index 0000000..56df1cd --- /dev/null +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m @@ -0,0 +1,303 @@ +#import "RoutingHTTPServer.h" +#import "RoutingConnection.h" +#import "Route.h" + +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Widiomatic-parentheses" + +@implementation RoutingHTTPServer { + NSMutableDictionary *routes; + NSMutableDictionary *defaultHeaders; + NSMutableDictionary *mimeTypes; + dispatch_queue_t routeQueue; +} + +@synthesize defaultHeaders; + +- (id)init { + if (self = [super init]) { + connectionClass = [RoutingConnection self]; + routes = [[NSMutableDictionary alloc] init]; + defaultHeaders = [[NSMutableDictionary alloc] init]; + [self setupMIMETypes]; + } + return self; +} + +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE +- (void)dealloc { + if (routeQueue) + dispatch_release(routeQueue); +} +#endif + +- (void)setDefaultHeaders:(NSDictionary *)headers { + if (headers) { + defaultHeaders = [headers mutableCopy]; + } else { + defaultHeaders = [[NSMutableDictionary alloc] init]; + } +} + +- (void)setDefaultHeader:(NSString *)field value:(NSString *)value { + [defaultHeaders setObject:value forKey:field]; +} + +- (dispatch_queue_t)routeQueue { + return routeQueue; +} + +- (void)setRouteQueue:(dispatch_queue_t)queue { +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + if (queue) + dispatch_retain(queue); + + if (routeQueue) + dispatch_release(routeQueue); +#endif + + routeQueue = queue; +} + +- (NSDictionary *)mimeTypes { + return mimeTypes; +} + +- (void)setMIMETypes:(NSDictionary *)types { + NSMutableDictionary *newTypes; + if (types) { + newTypes = [types mutableCopy]; + } else { + newTypes = [[NSMutableDictionary alloc] init]; + } + + mimeTypes = newTypes; +} + +- (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext { + [mimeTypes setObject:theType forKey:ext]; +} + +- (NSString *)mimeTypeForPath:(NSString *)path { + NSString *ext = [[path pathExtension] lowercaseString]; + if (!ext || [ext length] < 1) + return nil; + + return [mimeTypes objectForKey:ext]; +} + +- (void)get:(NSString *)path withBlock:(RequestHandler)block { + [self handleMethod:@"GET" withPath:path block:block]; +} + +- (void)post:(NSString *)path withBlock:(RequestHandler)block { + [self handleMethod:@"POST" withPath:path block:block]; +} + +- (void)put:(NSString *)path withBlock:(RequestHandler)block { + [self handleMethod:@"PUT" withPath:path block:block]; +} + +- (void)delete:(NSString *)path withBlock:(RequestHandler)block { + [self handleMethod:@"DELETE" withPath:path block:block]; +} + +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + block:(RequestHandler)block { + Route *route = [self routeWithPath:path]; + route.handler = block; + + [self addRoute:route forMethod:method]; +} + +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + target:(id)target + selector:(SEL)selector { + Route *route = [self routeWithPath:path]; + route.target = target; + route.selector = selector; + + [self addRoute:route forMethod:method]; +} + +- (void)addRoute:(Route *)route forMethod:(NSString *)method { + method = [method uppercaseString]; + NSMutableArray *methodRoutes = [routes objectForKey:method]; + if (methodRoutes == nil) { + methodRoutes = [NSMutableArray array]; + [routes setObject:methodRoutes forKey:method]; + } + + [methodRoutes addObject:route]; + + // Define a HEAD route for all GET routes + if ([method isEqualToString:@"GET"]) { + [self addRoute:route forMethod:@"HEAD"]; + } +} + +- (Route *)routeWithPath:(NSString *)path { + Route *route = [[Route alloc] init]; + NSMutableArray *keys = [NSMutableArray array]; + + if ([path length] > 2 && [path characterAtIndex:0] == '{') { + // This is a custom regular expression, just remove the {} + path = [path substringWithRange:NSMakeRange(1, [path length] - 2)]; + } else { + NSRegularExpression *regex = nil; + + // Escape regex characters + regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil]; + path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; + + // Parse any :parameters and * in the path + regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" + options:0 + error:nil]; + NSMutableString *regexPath = [NSMutableString stringWithString:path]; + __block NSInteger diff = 0; + [regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); + NSString *replacementString; + + NSString *capturedString = [path substringWithRange:result.range]; + if ([capturedString isEqualToString:@"*"]) { + [keys addObject:@"wildcards"]; + replacementString = @"(.*?)"; + } else { + NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]]; + [keys addObject:keyString]; + replacementString = @"([^/]+)"; + } + + [regexPath replaceCharactersInRange:replacementRange withString:replacementString]; + diff += replacementString.length - result.range.length; + }]; + + path = [NSString stringWithFormat:@"^%@$", regexPath]; + } + + route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil]; + if ([keys count] > 0) { + route.keys = keys; + } + + return route; +} + +- (BOOL)supportsMethod:(NSString *)method { + return ([routes objectForKey:method] != nil); +} + +- (void)handleRoute:(Route *)route + withRequest:(RouteRequest *)request + response:(RouteResponse *)response { + if (route.handler) { + route.handler(request, response); + } else { + id target = route.target; + SEL selector = route.selector; + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setArgument:&request atIndex:2]; + [invocation setArgument:&response atIndex:3]; + [invocation invokeWithTarget:target]; + } +} + +- (RouteResponse *)routeMethod:(NSString *)method + withPath:(NSString *)path + parameters:(NSDictionary *)params + request:(HTTPMessage *)httpMessage + connection:(HTTPConnection *)connection { + NSMutableArray *methodRoutes = [routes objectForKey:method]; + if (methodRoutes == nil) + return nil; + + for (Route *route in methodRoutes) { + NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)]; + if (!result) + continue; + + // The first range is all of the text matched by the regex. + NSUInteger captureCount = [result numberOfRanges]; + + if (route.keys) { + // Add the route's parameters to the parameter dictionary, accounting for + // the first range containing the matched text. + if (captureCount == [route.keys count] + 1) { + NSMutableDictionary *newParams = [params mutableCopy]; + NSUInteger index = 1; + BOOL firstWildcard = YES; + for (NSString *key in route.keys) { + NSString *capture = [path substringWithRange:[result rangeAtIndex:index]]; + if ([key isEqualToString:@"wildcards"]) { + NSMutableArray *wildcards = [newParams objectForKey:key]; + if (firstWildcard) { + // Create a new array and replace any existing object with the same key + wildcards = [NSMutableArray array]; + [newParams setObject:wildcards forKey:key]; + firstWildcard = NO; + } + [wildcards addObject:capture]; + } else { + [newParams setObject:capture forKey:key]; + } + index++; + } + params = newParams; + } + } else if (captureCount > 1) { + // For custom regular expressions place the anonymous captures in the captures parameter + NSMutableDictionary *newParams = [params mutableCopy]; + NSMutableArray *captures = [NSMutableArray array]; + for (NSUInteger i = 1; i < captureCount; i++) { + [captures addObject:[path substringWithRange:[result rangeAtIndex:i]]]; + } + [newParams setObject:captures forKey:@"captures"]; + params = newParams; + } + + RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params]; + RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection]; + if (!routeQueue) { + [self handleRoute:route withRequest:request response:response]; + } else { + // Process the route on the specified queue + dispatch_sync(routeQueue, ^{ + @autoreleasepool { + [self handleRoute:route withRequest:request response:response]; + } + }); + } + return response; + } + + return nil; +} + +- (void)setupMIMETypes { + mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + @"application/x-javascript", @"js", + @"image/gif", @"gif", + @"image/jpeg", @"jpg", + @"image/jpeg", @"jpeg", + @"image/png", @"png", + @"image/svg+xml", @"svg", + @"image/tiff", @"tif", + @"image/tiff", @"tiff", + @"image/x-icon", @"ico", + @"image/x-ms-bmp", @"bmp", + @"text/css", @"css", + @"text/html", @"html", + @"text/html", @"htm", + @"text/plain", @"txt", + @"text/xml", @"xml", + nil]; +} + +@end diff --git a/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/WebDriverAgentLib.h new file mode 100644 index 0000000..d0e3f73 --- /dev/null +++ b/WebDriverAgentLib/WebDriverAgentLib.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +//! Project version number for WebDriverAgentLib_. +FOUNDATION_EXPORT double WebDriverAgentLib_VersionNumber; + +//! Project version string for WebDriverAgentLib_. +FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/WebDriverAgentRunner/Info.plist b/WebDriverAgentRunner/Info.plist new file mode 100644 index 0000000..4612bd9 --- /dev/null +++ b/WebDriverAgentRunner/Info.plist @@ -0,0 +1,35 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSLocationAlwaysAndWhenInUseUsageDescription + + NSLocationAlwaysUsageDescription + + NSLocationWhenInUseUsageDescription + + + diff --git a/WebDriverAgentRunner/UITestingUITests.m b/WebDriverAgentRunner/UITestingUITests.m new file mode 100644 index 0000000..ad2ff45 --- /dev/null +++ b/WebDriverAgentRunner/UITestingUITests.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import +#import + +@interface UITestingUITests : FBFailureProofTestCase +@end + +@implementation UITestingUITests + ++ (void)setUp +{ + [FBDebugLogDelegateDecorator decorateXCTestLogger]; + [FBConfiguration disableRemoteQueryEvaluation]; + [FBConfiguration configureDefaultKeyboardPreferences]; + [FBConfiguration disableApplicationUIInterruptionsHandling]; + if (NSProcessInfo.processInfo.environment[@"ENABLE_AUTOMATIC_SCREEN_RECORDINGS"]) { + [FBConfiguration enableScreenRecordings]; + } else { + [FBConfiguration disableScreenRecordings]; + } + if (NSProcessInfo.processInfo.environment[@"ENABLE_AUTOMATIC_SCREENSHOTS"]) { + [FBConfiguration enableScreenshots]; + } else { + [FBConfiguration disableScreenshots]; + } + [super setUp]; +} + +/** + Never ending test used to start WebDriverAgent + */ +- (void)testRunner +{ + FBWebServer *webServer = [[FBWebServer alloc] init]; + webServer.delegate = self; + [webServer startServing]; +} + +#pragma mark - FBWebServerDelegate + +- (void)webServerDidRequestShutdown:(FBWebServer *)webServer +{ + [webServer stopServing]; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h new file mode 100644 index 0000000..847dbec --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@interface AppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m new file mode 100644 index 0000000..3ac33aa --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "AppDelegate.h" + +@interface AppDelegate () +@end + +@implementation AppDelegate +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h new file mode 100644 index 0000000..e5b2088 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@interface FBAlertViewController : UIViewController + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m new file mode 100644 index 0000000..1f1c77e --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBAlertViewController.h" + +#import +#import + +@interface FBAlertViewController () +@property (nonatomic, strong) CLLocationManager *locationManager; +@end + +@implementation FBAlertViewController + +- (IBAction)createAppAlert:(UIButton *)sender +{ + [self presentAlertController]; +} + +- (IBAction)createAppSheet:(UIButton *)sender +{ + UIAlertController *alerController = + [UIAlertController alertControllerWithTitle:@"Magic Sheet" + message:@"Should read" + preferredStyle:UIAlertControllerStyleActionSheet]; + UIPopoverPresentationController *popPresenter = [alerController popoverPresentationController]; + popPresenter.sourceView = sender; + [self presentViewController:alerController animated:YES completion:nil]; + +} + +- (IBAction)createNotificationAlert:(UIButton *)sender +{ + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound|UNAuthorizationOptionAlert|UNAuthorizationOptionBadge) + completionHandler:^(BOOL granted, NSError * _Nullable error) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] registerForRemoteNotifications]; + }); + }]; +} + +- (IBAction)createCameraRollAccessAlert:(UIButton *)sender +{ + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + }]; +} + +- (IBAction)createGPSAccessAlert:(UIButton *)sender +{ + self.locationManager = [CLLocationManager new]; + [self.locationManager requestAlwaysAuthorization]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; + for (UITouch *touch in touches) { + if (fabs(touch.maximumPossibleForce - touch.force) < 0.0001) { + [self presentAlertController]; + return; + } + } +} + +- (void)presentAlertController +{ + UIAlertController *alerController = + [UIAlertController alertControllerWithTitle:@"Magic" + message:@"Should read" + preferredStyle:UIAlertControllerStyleAlert]; + [alerController addAction:[UIAlertAction actionWithTitle:@"Will do" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alerController animated:YES completion:nil]; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h new file mode 100644 index 0000000..e9a6b8d --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#import + +@interface FBNavigationController : UINavigationController + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m new file mode 100644 index 0000000..3b0bec2 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBNavigationController.h" + +@implementation FBNavigationController + +#if !TARGET_OS_TV +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + return UIInterfaceOrientationMaskAll; +} +#endif + +- (BOOL)shouldAutorotate +{ + return YES; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h new file mode 100644 index 0000000..7cd517b --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface FBScrollViewController : UIViewController + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m new file mode 100644 index 0000000..a8fe797 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBScrollViewController.h" + +#import "FBTableDataSource.h" + +static const CGFloat FBSubviewHeight = 40.0; + +@interface FBScrollViewController () +@property (nonatomic, weak) IBOutlet UIScrollView *scrollView; +@property (nonatomic, strong) IBOutlet FBTableDataSource *dataSource; +@end + +@implementation FBScrollViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupLabelViews]; + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), self.dataSource.count * FBSubviewHeight); +} + +- (void)setupLabelViews +{ + NSUInteger count = self.dataSource.count; + for (NSInteger i = 0 ; i < count ; i++) { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, i * FBSubviewHeight, CGRectGetWidth(self.view.frame), FBSubviewHeight)]; + label.text = [self.dataSource textForElementAtIndex:i]; + label.textAlignment = NSTextAlignmentCenter; + [self.scrollView addSubview:label]; + } +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h new file mode 100644 index 0000000..d82f8b6 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface FBTableDataSource : NSObject + +- (NSUInteger)count; +- (NSString *)textForElementAtIndex:(NSInteger)index; + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m new file mode 100644 index 0000000..335f28a --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBTableDataSource.h" + +@implementation FBTableDataSource + +- (NSUInteger)count +{ + return 100; +} + +- (NSString *)textForElementAtIndex:(NSInteger)index +{ + return [NSString stringWithFormat:@"%ld", (long)index]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; + cell.textLabel.text = [self textForElementAtIndex:indexPath.row]; + return cell; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h new file mode 100644 index 0000000..ed6a9cd --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h @@ -0,0 +1,17 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TouchSpotView : UIView + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m new file mode 100644 index 0000000..ed79694 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m @@ -0,0 +1,28 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "TouchSpotView.h" + +@implementation TouchSpotView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = UIColor.lightGrayColor; + } + return self; +} + +- (void)setBounds:(CGRect)newBounds +{ + super.bounds = newBounds; + self.layer.cornerRadius = newBounds.size.width / 2.0; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h new file mode 100644 index 0000000..7125306 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h @@ -0,0 +1,23 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import +#import "TouchableView.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TouchViewController : UIViewController + +@property (weak, nonatomic) IBOutlet TouchableView *touchable; +@property (weak, nonatomic) IBOutlet UILabel *numberOfTapsLabel; +@property (weak, nonatomic) IBOutlet UILabel *numberOfTouchesLabel; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m new file mode 100644 index 0000000..25b3f99 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m @@ -0,0 +1,29 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "TouchViewController.h" + +@implementation TouchViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.touchable.delegate = self; + self.numberOfTouchesLabel.text = @"0"; + self.numberOfTapsLabel.text = @"0"; +} + +- (void)shouldHandleTapsNumber:(int)numberOfTaps { + self.numberOfTapsLabel.text = [NSString stringWithFormat:@"%d", numberOfTaps]; +} + +- (void)shouldHandleTouchesNumber:(int)touchesCount { + self.numberOfTouchesLabel.text = [NSString stringWithFormat:@"%d", touchesCount]; +} + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h new file mode 100644 index 0000000..53d0c18 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h @@ -0,0 +1,29 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import +#import "TouchSpotView.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol TouchableViewDelegate + +- (void)shouldHandleTouchesNumber:(int)touchesCount; +- (void)shouldHandleTapsNumber:(int)numberOfTaps; + +@end + +@interface TouchableView : UIView + +@property (nonatomic) NSMutableDictionary *touchViews; +@property (nonatomic) int numberOFTaps; +@property (nonatomic) id delegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m new file mode 100644 index 0000000..9e7412a --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m @@ -0,0 +1,108 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import "TouchableView.h" + +@implementation TouchableView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.multipleTouchEnabled = YES; + self.numberOFTaps = 0; + self.touchViews = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (self) { + self.multipleTouchEnabled = YES; + self.numberOFTaps = 0; + self.touchViews = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + self.numberOFTaps += 1; + [self.delegate shouldHandleTouchesNumber:(int)touches.count]; + for (UITouch *touch in touches) + { + [self createViewForTouch:touch]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + TouchSpotView *view = [self viewForTouch:touch]; + CGPoint newLocation = [touch locationInView:self]; + view.center = newLocation; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + [self removeViewForTouch:touch]; + } + [self.delegate shouldHandleTapsNumber:self.numberOFTaps]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + [self removeViewForTouch:touch]; + } +} + +- (void)createViewForTouch:(UITouch *)touch +{ + if (touch) + { + TouchSpotView *newView = [[TouchSpotView alloc] init]; + newView.bounds = CGRectMake(0, 0, 1, 1); + newView.center = [touch locationInView:self]; + [self addSubview:newView]; + [UIView animateWithDuration:0.2 animations:^{ + newView.bounds = CGRectMake(0, 0, 100, 100); + }]; + + self.touchViews[[self touchHash:touch]] = newView; + } +} + +- (TouchSpotView *)viewForTouch:(UITouch *)touch +{ + return self.touchViews[[self touchHash:touch]]; +} + +- (void)removeViewForTouch:(UITouch *)touch +{ + NSNumber *touchHash = [self touchHash:touch]; + UIView *view = self.touchViews[touchHash]; + if (view) + { + [view removeFromSuperview]; + [self.touchViews removeObjectForKey:touchHash]; + } +} + +- (NSNumber *)touchHash:(UITouch *)touch +{ + return [NSNumber numberWithUnsignedInteger:touch.hash]; +} +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h new file mode 100644 index 0000000..9349604 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface ViewController : UIViewController +@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m new file mode 100644 index 0000000..1267c28 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "ViewController.h" + +@interface ViewController () +@property (weak, nonatomic) IBOutlet UILabel *orentationLabel; +@end + +@implementation ViewController + +- (IBAction)deadlockApp:(id)sender +{ + dispatch_sync(dispatch_get_main_queue(), ^{ + // This will never execute + }); +} + +- (IBAction)didTapButton:(UIButton *)button +{ + button.selected = !button.selected; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + [self updateOrentationLabel]; +} + +#if !TARGET_OS_TV +- (void)updateOrentationLabel +{ + NSString *orientation = nil; + switch (UIDevice.currentDevice.orientation) { + case UIInterfaceOrientationPortrait: + orientation = @"Portrait"; + break; + case UIInterfaceOrientationPortraitUpsideDown: + orientation = @"PortraitUpsideDown"; + break; + case UIInterfaceOrientationLandscapeLeft: + orientation = @"LandscapeLeft"; + break; + case UIInterfaceOrientationLandscapeRight: + orientation = @"LandscapeRight"; + break; + case UIDeviceOrientationFaceUp: + orientation = @"FaceUp"; + break; + case UIDeviceOrientationFaceDown: + orientation = @"FaceDown"; + break; + case UIInterfaceOrientationUnknown: + orientation = @"Unknown"; + break; + } + self.orentationLabel.text = orientation; +} +#endif + +@end diff --git a/WebDriverAgentTests/IntegrationApp/Info.plist b/WebDriverAgentTests/IntegrationApp/Info.plist new file mode 100644 index 0000000..2dfb754 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationAlwaysAndWhenInUseUsageDescription + Yo Yo + NSLocationAlwaysUsageDescription + Yo Yo + NSLocationWhenInUseUsageDescription + Yo Yo + NSPhotoLibraryUsageDescription + Yo Yo + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard new file mode 100644 index 0000000..5fb805f --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDriverAgentTests/IntegrationApp/main.m b/WebDriverAgentTests/IntegrationApp/main.m new file mode 100644 index 0000000..58bd6a8 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/main.m @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, @"AppDelegate"); + } +} diff --git a/WebDriverAgentTests/IntegrationTests/FBAlertTests.m b/WebDriverAgentTests/IntegrationTests/FBAlertTests.m new file mode 100644 index 0000000..d02af4b --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBAlertTests.m @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +#import "FBConfiguration.h" +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "FBMacros.h" + +@interface FBAlertTests : FBIntegrationTestCase +@end + +@implementation FBAlertTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAlertsPage]; + [FBConfiguration disableApplicationUIInterruptionsHandling]; + }); + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + [super tearDown]; +} + +- (void)showApplicationAlert +{ + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count != 0); +} + +- (void)showApplicationSheet +{ + [self.testedApplication.buttons[FBShowSheetAlertButtonName] tap]; + FBAssertWaitTillBecomesTrue(self.testedApplication.sheets.count != 0); +} + +- (void)testAlertPresence +{ + FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertFalse(alert.isPresent); + [self showApplicationAlert]; + XCTAssertTrue(alert.isPresent); +} + +- (void)testAlertText +{ + FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertNil(alert.text); + [self showApplicationAlert]; + XCTAssertTrue([alert.text containsString:@"Magic"]); + XCTAssertTrue([alert.text containsString:@"Should read"]); +} + +- (void)testAlertLabels +{ + FBAlert* alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertNil(alert.buttonLabels); + [self showApplicationAlert]; + XCTAssertNotNil(alert.buttonLabels); + XCTAssertEqual(1, alert.buttonLabels.count); + XCTAssertEqualObjects(@"Will do", alert.buttonLabels[0]); +} + +- (void)testClickAlertButton +{ + FBAlert* alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertFalse([alert clickAlertButton:@"Invalid" error:nil]); + [self showApplicationAlert]; + XCTAssertFalse([alert clickAlertButton:@"Invalid" error:nil]); + FBAssertWaitTillBecomesTrue(alert.isPresent); + XCTAssertTrue([alert clickAlertButton:@"Will do" error:nil]); + FBAssertWaitTillBecomesTrue(!alert.isPresent); +} + +- (void)testAcceptingAlert +{ + NSError *error; + [self showApplicationAlert]; + XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] acceptWithError:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + XCTAssertNil(error); +} + +- (void)testAcceptingAlertWithCustomLocator +{ + NSError *error; + [self showApplicationAlert]; + [FBConfiguration setAcceptAlertButtonSelector:@"**/XCUIElementTypeButton[-1]"]; + @try { + XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] acceptWithError:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + XCTAssertNil(error); + } @finally { + [FBConfiguration setAcceptAlertButtonSelector:@""]; + } +} + +- (void)testDismissingAlert +{ + NSError *error; + [self showApplicationAlert]; + XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] dismissWithError:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + XCTAssertNil(error); +} + +- (void)testDismissingAlertWithCustomLocator +{ + NSError *error; + [self showApplicationAlert]; + [FBConfiguration setDismissAlertButtonSelector:@"**/XCUIElementTypeButton[-1]"]; + @try { + XCTAssertTrue([[FBAlert alertWithApplication:self.testedApplication] dismissWithError:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + XCTAssertNil(error); + } @finally { + [FBConfiguration setDismissAlertButtonSelector:@""]; + } +} + +- (void)testAlertElement +{ + [self showApplicationAlert]; + XCUIElement *alertElement = [FBAlert alertWithApplication:self.testedApplication].alertElement; + XCTAssertTrue(alertElement.exists); + XCTAssertTrue(alertElement.elementType == XCUIElementTypeAlert); +} + +- (void)testNotificationAlert +{ + FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertNil(alert.text); + [self.testedApplication.buttons[@"Create Notification Alert"] tap]; + FBAssertWaitTillBecomesTrue(alert.isPresent); + + XCTAssertTrue([alert.text containsString:@"Would Like to Send You Notifications"]); + XCTAssertTrue([alert.text containsString:@"Notifications may include"]); +} + +// This test case depends on the local app permission state. +- (void)testCameraRollAlert +{ + FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertNil(alert.text); + + [self.testedApplication.buttons[@"Create Camera Roll Alert"] tap]; + FBAssertWaitTillBecomesTrue(alert.isPresent); + + // "Would Like to Access Your Photos" or "Would Like to Access Your Photo Library" displayes on the alert button. + XCTAssertTrue([alert.text containsString:@"Would Like to Access Your Photo"]); + // iOS 15 has different UI flow + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { + [[FBAlert alertWithApplication:self.testedApplication] dismissWithError:nil]; + // CI env could take longer time to show up the button, thus it needs to wait a bit. + XCTAssertTrue([self.testedApplication.buttons[@"Cancel"] waitForExistenceWithTimeout:30.0]); + [self.testedApplication.buttons[@"Cancel"] tap]; + } +} + +- (void)testGPSAccessAlert +{ + FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; + XCTAssertNil(alert.text); + + [self.testedApplication.buttons[@"Create GPS access Alert"] tap]; + FBAssertWaitTillBecomesTrue(alert.isPresent); + + XCTAssertTrue([alert.text containsString:@"location"]); + XCTAssertTrue([alert.text containsString:@"Yo Yo"]); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m b/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m new file mode 100644 index 0000000..d086e35 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBMacros.h" +#import "FBSession.h" +#import "FBXCodeCompatibility.h" +#import "FBTestMacros.h" +#import "XCUIElement+FBUtilities.h" + + +@interface FBAutoAlertsHandlerTests : FBIntegrationTestCase +@property (nonatomic) FBSession *session; +@end + + +@implementation FBAutoAlertsHandlerTests + +- (void)setUp +{ + [super setUp]; + + [self launchApplication]; + [self goToAlertsPage]; + + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + + if (self.session) { + [self.session kill]; + } + + [super tearDown]; +} + +// The test is flaky on slow Travis CI +- (void)disabled_testAutoAcceptingOfAlerts +{ + self.session = [FBSession + initWithApplication:XCUIApplication.fb_activeApplication + defaultAlertAction:@"accept"]; + for (int i = 0; i < 2; i++) { + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + } +} + +// The test is flaky on slow Travis CI +- (void)disabled_testAutoDismissingOfAlerts +{ + self.session = [FBSession + initWithApplication:XCUIApplication.fb_activeApplication + defaultAlertAction:@"dismiss"]; + for (int i = 0; i < 2; i++) { + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); + } +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m b/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m new file mode 100644 index 0000000..0993aff --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m @@ -0,0 +1,38 @@ +/** +* Copyright (c) 2015-present, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ + +#import +#import "FBIntegrationTestCase.h" + +#import "FBConfiguration.h" +#import "FBRuntimeUtils.h" + +@interface FBConfigurationTests : FBIntegrationTestCase + +@end + +@implementation FBConfigurationTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; +} + +- (void)testReduceMotion +{ + BOOL defaultReduceMotionEnabled = [FBConfiguration reduceMotionEnabled]; + + [FBConfiguration setReduceMotionEnabled:YES]; + XCTAssertTrue([FBConfiguration reduceMotionEnabled]); + + [FBConfiguration setReduceMotionEnabled:defaultReduceMotionEnabled]; + XCTAssertEqual([FBConfiguration reduceMotionEnabled], defaultReduceMotionEnabled); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m new file mode 100644 index 0000000..9242638 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBFindElementCommands.h" +#import "FBTestMacros.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBAccessibility.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBWebDriverAttributes.h" + +@interface FBElementAttributeTests : FBIntegrationTestCase +@end + +@implementation FBElementAttributeTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAttributesPage]; + }); +} + +- (void)testElementAccessibilityAttributes +{ + // "Button" is accessibility element, and therefore isn't accessibility container + XCUIElement *buttonElement = self.testedApplication.buttons[@"Button"]; + XCTAssertTrue(buttonElement.exists); + XCTAssertTrue(buttonElement.fb_isAccessibilityElement); + XCTAssertFalse(buttonElement.isWDAccessibilityContainer); +} + +- (void)testContainerAccessibilityAttributes +{ + // "not_accessible" isn't accessibility element, but contains accessibility elements, so it is accessibility container + XCUIElement *inaccessibleButtonElement = self.testedApplication.buttons[@"not_accessible"]; + XCTAssertTrue(inaccessibleButtonElement.exists); + XCTAssertFalse(inaccessibleButtonElement.fb_isAccessibilityElement); + // FIXME: Xcode 11 environment returns false even if iOS 12 + // We must fix here to XCTAssertTrue if Xcode version will return the value properly + XCTAssertFalse(inaccessibleButtonElement.isWDAccessibilityContainer); +} + +- (void)testIgnoredAccessibilityAttributes +{ + // Images are neither accessibility elements nor contain them, so both checks should fail + XCUIElement *imageElement = self.testedApplication.images.allElementsBoundByIndex.firstObject; + XCTAssertTrue(imageElement.exists); + XCTAssertFalse(imageElement.fb_isAccessibilityElement); + XCTAssertFalse(imageElement.isWDAccessibilityContainer); +} + +- (void)testButtonAttributes +{ + XCUIElement *element = self.testedApplication.buttons[@"Button"]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeButton"); + XCTAssertEqualObjects(element.wdName, @"Button"); + XCTAssertEqualObjects(element.wdLabel, @"Button"); + XCTAssertNil(element.wdValue); + XCTAssertFalse(element.wdSelected); + XCTAssertTrue(element.fb_isVisible); + [element tap]; + XCTAssertTrue(element.wdValue.boolValue); + XCTAssertTrue(element.wdSelected); +} + +- (void)testLabelAttributes +{ + XCUIElement *element = self.testedApplication.staticTexts[@"Label"]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeStaticText"); + XCTAssertEqualObjects(element.wdName, @"Label"); + XCTAssertEqualObjects(element.wdLabel, @"Label"); + XCTAssertEqualObjects(element.wdValue, @"Label"); +} + +- (void)testIndexAttributes +{ + XCUIElement *element = self.testedApplication.buttons[@"Button"]; + XCTAssertTrue(element.exists); + XCTAssertEqual(element.wdIndex, 2); + XCUIElement *element2 = self.testedApplication; + XCTAssertTrue(element2.exists); + XCTAssertEqual(element2.wdIndex, 0); +} + +- (void)testAccessibilityTraits +{ + XCUIElement *button = self.testedApplication.buttons.firstMatch; + XCTAssertTrue(button.exists); + NSArray *buttonTraits = [button.wdTraits componentsSeparatedByString:@", "]; + NSArray *expectedButtonTraits = @[@"Button"]; + XCTAssertEqual(buttonTraits.count, expectedButtonTraits.count, @"Button should have exactly 1 trait"); + XCTAssertEqualObjects(buttonTraits, expectedButtonTraits); + XCTAssertEqualObjects(button.wdType, @"XCUIElementTypeButton"); + + XCUIElement *toggle = self.testedApplication.switches.firstMatch; + XCTAssertTrue(toggle.exists); + + // iOS 17.0 specific traits if available + NSArray *toggleTraits = [toggle.wdTraits componentsSeparatedByString:@", "]; + NSArray *expectedToggleTraits; + + #if __clang_major__ >= 16 + if (@available(iOS 17.0, *)) { + expectedToggleTraits = @[@"ToggleButton", @"Button"]; + XCTAssertEqual(toggleTraits.count, 2, @"Toggle should have exactly 2 traits on iOS 17+"); + } + #else + expectedToggleTraits = @[@"Button"]; + XCTAssertEqual(toggleTraits.count, 1, @"Toggle should have exactly 1 trait on iOS < 17"); + #endif + XCTAssertEqualObjects(toggleTraits, expectedToggleTraits); + XCTAssertEqualObjects(toggle.wdType, @"XCUIElementTypeSwitch"); + + XCUIElement *slider = self.testedApplication.sliders.firstMatch; + XCTAssertTrue(slider.exists); + NSArray *sliderTraits = [slider.wdTraits componentsSeparatedByString:@", "]; + NSArray *expectedSliderTraits = @[@"Adjustable"]; + XCTAssertEqual(sliderTraits.count, expectedSliderTraits.count, @"Slider should have exactly 1 trait"); + XCTAssertEqualObjects(sliderTraits, expectedSliderTraits); + XCTAssertEqualObjects(slider.wdType, @"XCUIElementTypeSlider"); + + XCUIElement *picker = self.testedApplication.pickerWheels.firstMatch; + XCTAssertTrue(picker.exists); + NSArray *pickerTraits = [picker.wdTraits componentsSeparatedByString:@", "]; + NSArray *expectedPickerTraits = @[@"Adjustable"]; + XCTAssertEqual(pickerTraits.count, expectedPickerTraits.count, @"Picker should have exactly 1 trait"); + XCTAssertEqualObjects(pickerTraits, expectedPickerTraits); + XCTAssertEqualObjects(picker.wdType, @"XCUIElementTypePickerWheel"); +} + +- (void)testTextFieldAttributes +{ + XCUIElement *element = self.testedApplication.textFields[@"Value"]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextField"); + XCTAssertNil(element.wdName); + XCTAssertEqualObjects(element.wdLabel, @""); + XCTAssertEqualObjects(element.wdValue, @"Value"); +} + +- (void)testTextFieldWithAccessibilityIdentifiersAttributes +{ + XCUIElement *element = self.testedApplication.textFields[@"aIdentifier"]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextField"); + XCTAssertEqualObjects(element.wdName, @"aIdentifier"); + XCTAssertEqualObjects(element.wdLabel, @"aLabel"); + XCTAssertEqualObjects(element.wdValue, @"Value2"); +} + +- (void)testSegmentedControlAttributes +{ + XCUIElement *element = self.testedApplication.segmentedControls.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSegmentedControl"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertNil(element.wdValue); +} + +- (void)testSliderAttributes +{ + XCUIElement *element = self.testedApplication.sliders.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSlider"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertTrue([element.wdValue containsString:@"50"]); + + NSNumber *minValue = element.wdMinValue; + NSNumber *maxValue = element.wdMaxValue; + + XCTAssertNotNil(minValue, @"Slider minValue should not be nil"); + XCTAssertNotNil(maxValue, @"Slider maxValue should not be nil"); + + XCTAssertEqualObjects(minValue, @0); + XCTAssertEqualObjects(maxValue, @1); +} + + +- (void)testActivityIndicatorAttributes +{ + XCUIElement *element = self.testedApplication.activityIndicators.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeActivityIndicator"); + XCTAssertEqualObjects(element.wdName, @"Progress halted"); + XCTAssertEqualObjects(element.wdLabel, @"Progress halted"); + XCTAssertEqualObjects(element.wdValue, @"0"); +} + +- (void)testSwitchAttributes +{ + XCUIElement *element = self.testedApplication.switches.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeSwitch"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertNil(element.wdPlaceholderValue); + XCTAssertEqualObjects(element.wdValue, @"1"); + XCTAssertFalse(element.wdSelected); + XCTAssertEqual(element.wdHittable, element.hittable); + [element tap]; + XCTAssertEqualObjects(element.wdValue, @"0"); + XCTAssertFalse(element.wdSelected); +} + +- (void)testPickerWheelAttributes +{ + XCUIElement *element = self.testedApplication.pickerWheels[@"Today"]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertEqualObjects(element.wdValue, @"Today"); +} + +- (void)testPageIndicatorAttributes +{ + XCUIElement *element = self.testedApplication.pageIndicators.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePageIndicator"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertEqualObjects(element.wdValue, @"page 1 of 3"); +} + +- (void)testTextViewAttributes +{ + XCUIElement *element = self.testedApplication.textViews.element; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypeTextView"); + XCTAssertNil(element.wdName); + XCTAssertNil(element.wdLabel); + XCTAssertEqualObjects(element.wdValue, @"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m b/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m new file mode 100644 index 0000000..f652c81 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBSwiping.h" + +@interface FBElementSwipingTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *scrollView; +- (void)openScrollView; +@end + +@implementation FBElementSwipingTests + +- (void)openScrollView +{ + [self launchApplication]; + [self goToScrollPageWithCells:YES]; + self.scrollView = [[self.testedApplication.query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"scrollView"].element; +} + +- (void)setUp +{ + [super setUp]; + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { + [self openScrollView]; + } else { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self openScrollView]; + }); + } +} + +- (void)tearDown +{ + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { + // Move to top page once to reset the scroll place + // since iOS 15 seems cannot handle cell visibility well when the view keps the view + [self.testedApplication terminate]; + } +} + +- (void)testSwipeUp +{ + [self.scrollView fb_swipeWithDirection:@"up" velocity:nil]; + FBAssertInvisibleCell(@"0"); +} + +- (void)testSwipeDown +{ + [self.scrollView fb_swipeWithDirection:@"up" velocity:nil]; + FBAssertInvisibleCell(@"0"); + [self.scrollView fb_swipeWithDirection:@"down" velocity:nil]; + FBAssertVisibleCell(@"0"); +} + +- (void)testSwipeDownWithVelocity +{ + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } + [self.scrollView fb_swipeWithDirection:@"up" velocity:@2500]; + FBAssertInvisibleCell(@"0"); + [self.scrollView fb_swipeWithDirection:@"down" velocity:@3000]; + FBAssertVisibleCell(@"0"); +} + +@end + +@interface FBElementSwipingApplicationTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *scrollView; +- (void)openScrollView; +@end + +@implementation FBElementSwipingApplicationTests + +- (void)openScrollView +{ + [self launchApplication]; + [self goToScrollPageWithCells:YES]; +} + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self openScrollView]; + }); +} + +- (void)testSwipeUp +{ + [self.testedApplication fb_swipeWithDirection:@"up" velocity:nil]; + FBAssertInvisibleCell(@"0"); +} + +- (void)testSwipeDown +{ + [self.testedApplication fb_swipeWithDirection:@"up" velocity:nil]; + FBAssertInvisibleCell(@"0"); + [self.testedApplication fb_swipeWithDirection:@"down" velocity:nil]; + FBAssertVisibleCell(@"0"); +} + +- (void)testSwipeDownWithVelocity +{ + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } + [self.testedApplication fb_swipeWithDirection:@"up" velocity:@2500]; + FBAssertInvisibleCell(@"0"); + [self.testedApplication fb_swipeWithDirection:@"down" velocity:@2500]; + FBAssertVisibleCell(@"0"); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m b/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m new file mode 100644 index 0000000..87a9094 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBMacros.h" +#import "FBTestMacros.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBIsVisible.h" + +@interface FBElementVisibilityTests : FBIntegrationTestCase +@end + +@implementation FBElementVisibilityTests + +- (void)testSpringBoardIcons +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + return; + } + [self launchApplication]; + [self goToSpringBoardFirstPage]; + + // Check Icons on first screen + // Note: Calender app exits 2 (an app icon + a widget) exist on the home screen + // on iOS 15+. The firstMatch is for it. + XCTAssertTrue(self.springboard.icons[@"Calendar"].firstMatch.fb_isVisible); + XCTAssertTrue(self.springboard.icons[@"Reminders"].fb_isVisible); + + // Check Icons on second screen screen + XCTAssertFalse(self.springboard.icons[@"IntegrationApp"].firstMatch.fb_isVisible); +} + +- (void)testSpringBoardSubfolder +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad + || SYSTEM_VERSION_GREATER_THAN(@"12.0")) { + return; + } + [self launchApplication]; + [self goToSpringBoardExtras]; + XCTAssertFalse(self.springboard.icons[@"Extras"].otherElements[@"Contacts"].fb_isVisible); +} + +- (void)disabled_testIconsFromSearchDashboard +{ + // This test causes: + // Failure fetching attributes for element Device element: Error Domain=XCTDaemonErrorDomain Code=13 "Value for attribute 5017 is an error." UserInfo={NSLocalizedDescription=Value for attribute 5017 is an error.} + [self launchApplication]; + [self goToSpringBoardDashboard]; + XCTAssertFalse(self.springboard.icons[@"Reminders"].fb_isVisible); + XCTAssertFalse([[[self.springboard descendantsMatchingType:XCUIElementTypeIcon] + matchingIdentifier:@"IntegrationApp"] + firstMatch].fb_isVisible); +} + +- (void)testTableViewCells +{ + [self launchApplication]; + [self goToScrollPageWithCells:YES]; + for (int i = 0 ; i < 10 ; i++) { + FBAssertWaitTillBecomesTrue(self.testedApplication.cells.allElementsBoundByIndex[i].fb_isVisible); + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts.allElementsBoundByIndex[i].fb_isVisible); + } +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m b/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m new file mode 100644 index 0000000..8355c3d --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBFailureProofTestCaseTests.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBFailureProofTestCase.h" +#import "FBExceptionHandler.h" + +@interface FBFailureProofTestCaseTests : FBFailureProofTestCase +@end + +@implementation FBFailureProofTestCaseTests + +- (void)setUp +{ + [super setUp]; + [[XCUIApplication new] launch]; +} + +- (void)testPreventElementSearchFailure +{ + [[XCUIApplication new].buttons[@"kaboom"] tap]; +} + +- (void)testInactiveAppSearch +{ + [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; + [[XCUIApplication new].buttons[@"kaboom"] tap]; +} + +- (void)testPreventAssertFailure +{ + XCTAssertNotNil(nil); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m b/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m new file mode 100644 index 0000000..5127321 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" + +#import "FBMacros.h" +#import "FBElementCache.h" +#import "FBTestMacros.h" +#import "XCUIDevice.h" +#import "XCUIDevice+FBRotation.h" +#import "XCUIElement+FBForceTouch.h" +#import "XCUIElement+FBIsVisible.h" + +@interface FBForceTouchTests : FBIntegrationTestCase +@end + +// It is recommnded to verify these tests with different iOS versions + +@implementation FBForceTouchTests + +- (void)verifyForceTapWithOrientation:(UIDeviceOrientation)orientation +{ + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + NSError *error; + XCTAssertTrue(self.testedApplication.alerts.count == 0); + [self.testedApplication.buttons[FBShowAlertForceTouchButtonName] fb_forceTouchCoordinate:nil + pressure:nil + duration:nil + error:&error]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); +} + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAlertsPage]; + }); + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + [self resetOrientation]; + [super tearDown]; +} + +- (void)testForceTap +{ + if (![XCUIDevice sharedDevice].supportsPressureInteraction) { + return; + } + + [self verifyForceTapWithOrientation:UIDeviceOrientationPortrait]; +} + +- (void)testForceTapInLandscapeLeft +{ + if (![XCUIDevice sharedDevice].supportsPressureInteraction) { + return; + } + + [self verifyForceTapWithOrientation:UIDeviceOrientationLandscapeLeft]; +} + +- (void)testForceTapInLandscapeRight +{ + if (![XCUIDevice sharedDevice].supportsPressureInteraction) { + return; + } + + [self verifyForceTapWithOrientation:UIDeviceOrientationLandscapeRight]; +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m b/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m new file mode 100644 index 0000000..4eddbc4 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBImageProcessor.h" +#import "FBIntegrationTestCase.h" + + +@interface FBImageProcessorTests : FBIntegrationTestCase + +@property (nonatomic) NSData *originalImage; +@property (nonatomic) CGSize originalSize; + +@end + +@implementation FBImageProcessorTests + +- (void)setUp { + XCUIApplication *app = [[XCUIApplication alloc] init]; + [app launch]; + XCUIScreenshot *screenshot = app.screenshot; + self.originalImage = UIImageJPEGRepresentation(screenshot.image, 1.0); + self.originalSize = [FBImageProcessorTests scaledSizeFromImage:screenshot.image]; +} + +- (void)testScaling { + CGFloat halfScale = 0.5; + CGSize expectedHalfScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.5]; + [self scaleImageWithFactor:halfScale + expectedSize:expectedHalfScaleSize]; + + // 0 is the smalles scaling factor we accept + CGFloat minScale = 0.0; + CGSize expectedMinScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.01]; + [self scaleImageWithFactor:minScale + expectedSize:expectedMinScaleSize]; + + // For scaling factors above 100 we don't perform any scaling and just return the unmodified image + [self scaleImageWithFactor:1.0 + expectedSize:self.originalSize]; + [self scaleImageWithFactor:2.0 + expectedSize:self.originalSize]; +} + +- (void)scaleImageWithFactor:(CGFloat)scalingFactor expectedSize:(CGSize)excpectedSize { + FBImageProcessor *scaler = [[FBImageProcessor alloc] init]; + + id expScaled = [self expectationWithDescription:@"Receive scaled image"]; + + [scaler submitImageData:self.originalImage + scalingFactor:scalingFactor + completionHandler:^(NSData *scaled) { + UIImage *scaledImage = [UIImage imageWithData:scaled]; + CGSize scaledSize = [FBImageProcessorTests scaledSizeFromImage:scaledImage]; + + XCTAssertEqualWithAccuracy(scaledSize.width, excpectedSize.width, 1.0); + XCTAssertEqualWithAccuracy(scaledSize.height, excpectedSize.height, 1.0); + + [expScaled fulfill]; + }]; + + [self waitForExpectations:@[expScaled] + timeout:0.5]; + +} + ++ (CGSize)scaledSizeFromImage:(UIImage *)image { + return CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); +} + ++ (CGSize)sizeFromSize:(CGSize)size scalingFactor:(CGFloat)scalingFactor { + return CGSizeMake(round(size.width * scalingFactor), round(size.height * scalingFactor)); +} + +@end + diff --git a/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h b/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h new file mode 100644 index 0000000..9c7ee57 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +extern NSString *const FBShowAlertButtonName; +extern NSString *const FBShowSheetAlertButtonName; +extern NSString *const FBShowAlertForceTouchButtonName; +extern NSString *const FBTouchesCountLabelIdentifier; +extern NSString *const FBTapsCountLabelIdentifier; + +/** + XCTestCase helper class used for integration tests + */ +@interface FBIntegrationTestCase : XCTestCase +@property (nonatomic, strong, readonly) XCUIApplication *testedApplication; +@property (nonatomic, strong, readonly) XCUIApplication *springboard; + +/** + Launches application and resets side effects of testing like orientation etc. + */ +- (void)launchApplication; + +/** + Navigates integration app to attributes page + */ +- (void)goToAttributesPage; + +/** + Navigates integration app to alerts page + */ +- (void)goToAlertsPage; + +/** + Navigates integration app to touch page + */ +- (void)goToTouchPage; + +/** + Navigates to SpringBoard first page + */ +- (void)goToSpringBoardFirstPage; + +/** + Navigates to SpringBoard path with Extras folder + */ +- (void)goToSpringBoardExtras; + +/** + Navigates to SpringBoard's dashboard + */ +- (void)goToSpringBoardDashboard; + +/** + Navigates integration app to scrolling page + @param showCells whether should navigate to view with cell or plain scrollview + */ +- (void)goToScrollPageWithCells:(BOOL)showCells; + +/** + Verifies no alerts are present on the page. + If an alert exists then it is going to be dismissed. + */ +- (void)clearAlert; + +/** + Resets device orientation to portrait mode + */ +- (void)resetOrientation; + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m b/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m new file mode 100644 index 0000000..bfdd3db --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBAlert.h" +#import "FBTestMacros.h" +#import "FBIntegrationTestCase.h" +#import "FBConfiguration.h" +#import "FBMacros.h" +#import "FBRunLoopSpinner.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice+FBRotation.h" +#import "XCUIElement.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestConfiguration.h" + +NSString *const FBShowAlertButtonName = @"Create App Alert"; +NSString *const FBShowSheetAlertButtonName = @"Create Sheet Alert"; +NSString *const FBShowAlertForceTouchButtonName = @"Create Alert (Force Touch)"; +NSString *const FBTouchesCountLabelIdentifier = @"numberOfTouchesLabel"; +NSString *const FBTapsCountLabelIdentifier = @"numberOfTapsLabel"; + +@interface FBIntegrationTestCase () +@property (nonatomic, strong) XCUIApplication *testedApplication; +@property (nonatomic, strong) XCUIApplication *springboard; +@end + +@implementation FBIntegrationTestCase + +- (void)setUp +{ + // Enable it to get extended XCTest logs printed into the console + // [FBConfiguration enableXcTestDebugLogs]; + [super setUp]; + [FBConfiguration disableRemoteQueryEvaluation]; + [FBConfiguration disableAttributeKeyPathAnalysis]; + [FBConfiguration configureDefaultKeyboardPreferences]; + [FBConfiguration disableApplicationUIInterruptionsHandling]; + [FBConfiguration disableScreenshots]; + self.continueAfterFailure = NO; + self.springboard = XCUIApplication.fb_systemApplication; + self.testedApplication = [XCUIApplication new]; +} + +- (void)resetOrientation +{ + if ([XCUIDevice sharedDevice].orientation != UIDeviceOrientationPortrait) { + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationPortrait]; + } +} + +- (void)launchApplication +{ + [self.testedApplication launch]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Alerts"].fb_isVisible); +} + +- (void)goToAttributesPage +{ + [self.testedApplication.buttons[@"Attributes"] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Button"].fb_isVisible); +} + +- (void)goToAlertsPage +{ + [self.testedApplication.buttons[@"Alerts"] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[FBShowAlertButtonName].fb_isVisible); + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[FBShowSheetAlertButtonName].fb_isVisible); +} + +- (void)goToTouchPage +{ + [self.testedApplication.buttons[@"Touch"] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[FBTouchesCountLabelIdentifier].fb_isVisible); + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[FBTapsCountLabelIdentifier].fb_isVisible); +} + +- (void)goToSpringBoardFirstPage +{ + [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(XCUIApplication.fb_systemApplication.icons[@"Safari"].exists); + [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(XCUIApplication.fb_systemApplication.icons[@"Calendar"].firstMatch.fb_isVisible); +} + +- (void)goToSpringBoardExtras +{ + [self goToSpringBoardFirstPage]; + [self.springboard swipeLeft]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.springboard.icons[@"Extras"].fb_isVisible); +} + +- (void)goToSpringBoardDashboard +{ + [self goToSpringBoardFirstPage]; + [self.springboard swipeRight]; + [self.testedApplication fb_waitUntilStable]; + NSPredicate *predicate = + [NSPredicate predicateWithFormat: + @"%K IN %@", + FBStringify(XCUIElement, identifier), + @[@"SBSearchEtceteraIsolatedView", @"SpotlightSearchField"] + ]; + FBAssertWaitTillBecomesTrue([[self.springboard descendantsMatchingType:XCUIElementTypeAny] elementMatchingPredicate:predicate].fb_isVisible); + FBAssertWaitTillBecomesTrue(!self.springboard.icons[@"Calendar"].fb_isVisible); +} + +- (void)goToScrollPageWithCells:(BOOL)showCells +{ + [self.testedApplication.buttons[@"Scrolling"] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"TableView"].fb_isVisible); + [self.testedApplication.buttons[showCells ? @"TableView": @"ScrollView"] tap]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); +} + +- (void)clearAlert +{ + [self.testedApplication fb_waitUntilStable]; + [[FBAlert alertWithApplication:self.testedApplication] dismissWithError:nil]; + [self.testedApplication fb_waitUntilStable]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m b/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m new file mode 100644 index 0000000..5b8bf31 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBMacros.h" +#import "FBIntegrationTestCase.h" +#import "FBKeyboard.h" +#import "FBRunLoopSpinner.h" +#import "XCUIApplication+FBHelpers.h" + +@interface FBKeyboardTests : FBIntegrationTestCase +@end + +@implementation FBKeyboardTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToAttributesPage]; +} + +- (void)testKeyboardDismissal +{ + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + + if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { + // A workaround until find out to clear tutorial on iOS 15 + XCUIElement *textField = self.testedApplication.staticTexts[@"Continue"]; + if (textField.hittable) { + [textField tap]; + } + } + + NSError *error; + XCTAssertTrue([FBKeyboard waitUntilVisibleForApplication:self.testedApplication timeout:1 error:&error]); + XCTAssertNil(error); + if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + XCTAssertTrue([self.testedApplication fb_dismissKeyboardWithKeyNames:nil error:&error]); + XCTAssertNil(error); + } else { + XCTAssertFalse([self.testedApplication fb_dismissKeyboardWithKeyNames:@[@"return"] error:&error]); + XCTAssertNotNil(error); + } +} + +- (void)testKeyboardPresenceVerification +{ + NSError *error; + XCTAssertFalse([FBKeyboard waitUntilVisibleForApplication:self.testedApplication timeout:1 error:&error]); + XCTAssertNotNil(error); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m b/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m new file mode 100644 index 0000000..73e9275 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "XCUIElement+FBTyping.h" +#import "FBPasteboard.h" +#import "FBTestMacros.h" +#import "FBXCodeCompatibility.h" + +@interface FBPasteboardTests : FBIntegrationTestCase +@end + +@implementation FBPasteboardTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToAttributesPage]; +} + +- (void)testSetPasteboard +{ + NSString *text = @"Happy pasting"; + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + NSError *error; + BOOL result = [FBPasteboard setData:(NSData *)[text dataUsingEncoding:NSUTF8StringEncoding] + forType:@"plaintext" + error:&error]; + XCTAssertTrue(result); + XCTAssertNil(error); + [textField tap]; + XCTAssertTrue([textField fb_clearTextWithError:&error]); + [textField pressForDuration:2.0]; + XCUIElementQuery *pastItemsQuery = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"Paste"]; + if (![pastItemsQuery.firstMatch waitForExistenceWithTimeout:2.0]) { + XCTFail(@"No matched element named 'Paste'"); + } + XCUIElement *pasteItem = pastItemsQuery.firstMatch; + XCTAssertNotNil(pasteItem); + [pasteItem tap]; + FBAssertWaitTillBecomesTrue([textField.value isEqualToString:text]); +} + +- (void)testGetPasteboard +{ + NSString *text = @"Happy copying"; + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + NSError *error; + XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); + [textField pressForDuration:2.0]; + XCUIElement *selectAllItem = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:@"Select All"].firstMatch; + XCTAssertTrue([selectAllItem waitForExistenceWithTimeout:5]); + [selectAllItem tap]; + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + [textField pressForDuration:2.0]; + } + XCUIElement *copyItem = [[self.testedApplication descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:@"Copy"].firstMatch; + XCTAssertTrue([copyItem waitForExistenceWithTimeout:5]); + [copyItem tap]; + FBWaitExact(1.0); + NSData *result = [FBPasteboard dataForType:@"plaintext" error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(textField.value, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); +} + +- (void)testUrlCopyPaste +{ + NSString *urlString = @"http://appium.io?some=value"; + NSError *error; + XCTAssertTrue([FBPasteboard setData:(NSData *)[urlString dataUsingEncoding:NSUTF8StringEncoding] + forType:@"url" + error:&error]); + XCTAssertNil(error); + NSData *result = [FBPasteboard dataForType:@"url" error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(urlString, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); +} + +@end + + diff --git a/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m b/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m new file mode 100644 index 0000000..3b32df4 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "XCUIElement+FBPickerWheel.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "FBXCodeCompatibility.h" + +@interface FBPickerWheelSelectTests : FBIntegrationTestCase +@end + +@implementation FBPickerWheelSelectTests + +static const CGFloat DEFAULT_OFFSET = (CGFloat)0.2; + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAttributesPage]; + }); +} + +- (void)testSelectNextPickerValue +{ + XCUIElement *element = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); + NSError *error; + NSString *previousValue = element.wdValue; + XCTAssertTrue([element fb_selectNextOptionWithOffset:DEFAULT_OFFSET error:&error]); + XCTAssertNotEqualObjects(previousValue, element.wdValue); +} + +- (void)testSelectPreviousPickerValue +{ + XCUIElement *element = [self.testedApplication.pickerWheels elementBoundByIndex:1]; + XCTAssertTrue(element.exists); + XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); + NSError *error; + NSString *previousValue = element.wdValue; + XCTAssertTrue([element fb_selectPreviousOptionWithOffset:DEFAULT_OFFSET error:&error]); + XCTAssertNotEqualObjects(previousValue, element.wdValue); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m b/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m new file mode 100644 index 0000000..a9c6ddc --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "FBMacros.h" +#import "FBSession.h" +#import "FBXCodeCompatibility.h" +#import "XCUIElement+FBTyping.h" +#import "FBAlert.h" +#import "XCUIApplication+FBAlert.h" + + +@interface FBSafariAlertIntegrationTests : FBIntegrationTestCase +@property (nonatomic) FBSession *session; +@property (nonatomic) XCUIApplication *safariApp; +@end + + +@implementation FBSafariAlertIntegrationTests + +- (void)setUp +{ + [super setUp]; + self.session = [FBSession initWithApplication:XCUIApplication.fb_activeApplication]; + [self.session launchApplicationWithBundleId:FB_SAFARI_BUNDLE_ID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + self.safariApp = self.session.activeApplication; +} + +- (void)tearDown +{ + [self.session terminateApplicationWithBundleId:FB_SAFARI_BUNDLE_ID]; +} + +- (void)disabled_testCanHandleSafariInputPrompt +{ + XCUIElement *urlInput = [[self.safariApp + descendantsMatchingType:XCUIElementTypeTextField] + matchingPredicate:[ + NSPredicate predicateWithFormat:@"label == 'Address' or label == 'URL'" + ]].firstMatch; + if (!urlInput.exists) { + [[[self.safariApp descendantsMatchingType:XCUIElementTypeButton] matchingPredicate:[ + NSPredicate predicateWithFormat:@"label == 'Address' or label == 'URL'"]].firstMatch tap]; + } + XCTAssertTrue([urlInput fb_typeText:@"https://www.w3schools.com/js/tryit.asp?filename=tryjs_alert" + shouldClear:YES + error:nil]); + [[[self.safariApp descendantsMatchingType:XCUIElementTypeButton] matchingIdentifier:@"Go"].firstMatch tap]; + XCUIElement *clickMeButton = [[self.safariApp descendantsMatchingType:XCUIElementTypeButton] + matchingPredicate:[NSPredicate predicateWithFormat:@"label == 'Try it'"]].firstMatch; + XCTAssertTrue([clickMeButton waitForExistenceWithTimeout:15.0]); + [clickMeButton tap]; + FBAlert *alert = [FBAlert alertWithApplication:self.safariApp]; + FBAssertWaitTillBecomesTrue([alert.text isEqualToString:@"I am an alert box!"]); + NSArray *buttonLabels = alert.buttonLabels; + XCTAssertEqualObjects(buttonLabels.firstObject, @"Close"); + XCTAssertNotNil([self.safariApp fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Close']" + shouldReturnAfterFirstMatch:YES].firstObject); + XCTAssertTrue([alert acceptWithError:nil]); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBScreenTests.m b/WebDriverAgentTests/IntegrationTests/FBScreenTests.m new file mode 100644 index 0000000..4f335f9 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBScreenTests.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBScreen.h" + +@interface FBScreenTests : FBIntegrationTestCase +@end + +@implementation FBScreenTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; +} + +- (void)testScreenScale +{ + XCTAssertTrue([FBScreen scale] >= 2); +} + +@end + diff --git a/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m b/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m new file mode 100644 index 0000000..915ca7b --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" + +#import "FBMacros.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBScrolling.h" +#import "XCUIElement+FBClassChain.h" +#import "FBXCodeCompatibility.h" + + +@interface FBScrollingTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *scrollView; +@end + +@implementation FBScrollingTests + ++ (BOOL)shouldShowCells +{ + return YES; +} + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToScrollPageWithCells:YES]; + self.scrollView = [[self.testedApplication.query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:@"scrollView"].element; +} + +- (void)testCellVisibility +{ + FBAssertVisibleCell(@"0"); + FBAssertVisibleCell(@"10"); + XCUIElement *cell10 = FBCellElementWithLabel(@"10"); + XCTAssertEqual([cell10 isWDHittable], [cell10 isHittable]); + FBAssertInvisibleCell(@"30"); + FBAssertInvisibleCell(@"50"); + XCUIElement *cell50 = FBCellElementWithLabel(@"50"); + XCTAssertEqual([cell50 isWDHittable], [cell50 isHittable]); +} + +- (void)testSimpleScroll +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + + FBAssertVisibleCell(@"0"); + FBAssertVisibleCell(@"10"); + [self.scrollView fb_scrollDownByNormalizedDistance:1.0]; + FBAssertInvisibleCell(@"0"); + FBAssertInvisibleCell(@"10"); + XCTAssertTrue(self.testedApplication.staticTexts.count > 0); + [self.scrollView fb_scrollUpByNormalizedDistance:1.0]; + FBAssertVisibleCell(@"0"); + FBAssertVisibleCell(@"10"); +} + +- (void)testScrollToVisible +{ + NSString *cellName = @"30"; + FBAssertInvisibleCell(cellName); + NSError *error; + XCTAssertTrue([FBCellElementWithLabel(cellName) fb_scrollToVisibleWithError:&error]); + XCTAssertNil(error); + FBAssertVisibleCell(cellName); +} + +- (void)testFarScrollToVisible +{ + NSString *cellName = @"80"; + NSError *error; + FBAssertInvisibleCell(cellName); + XCTAssertTrue([FBCellElementWithLabel(cellName) fb_scrollToVisibleWithError:&error]); + XCTAssertNil(error); + FBAssertVisibleCell(cellName); +} + +- (void)testNativeFarScrollToVisible +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + + NSString *cellName = @"80"; + NSError *error; + FBAssertInvisibleCell(cellName); + XCTAssertTrue([FBCellElementWithLabel(cellName) fb_nativeScrollToVisibleWithError:&error]); + XCTAssertNil(error); + FBAssertVisibleCell(cellName); +} + +- (void)testAttributeWithNullScrollToVisible +{ + NSError *error; + NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/XCUIElementTypeTable/XCUIElementTypeCell[60]" shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(queryMatches.count, 1); + XCUIElement *element = queryMatches.firstObject; + XCTAssertFalse(element.fb_isVisible); + [element fb_scrollToVisibleWithError:&error]; + XCTAssertNil(error); + XCTAssertTrue(element.fb_isVisible); + + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + + [element tap]; + XCTAssertTrue(element.wdSelected); +} + +@end + diff --git a/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m new file mode 100644 index 0000000..d12495e --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBExceptions.h" +#import "FBMacros.h" +#import "FBSession.h" +#import "FBXCodeCompatibility.h" +#import "FBTestMacros.h" +#import "FBUnattachedAppLauncher.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIApplication.h" + +@interface FBSession (Tests) + +@end + +@interface FBSessionIntegrationTests : FBIntegrationTestCase +@property (nonatomic) FBSession *session; +@end + + +static NSString *const SETTINGS_BUNDLE_ID = @"com.apple.Preferences"; + +@implementation FBSessionIntegrationTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:self.testedApplication.bundleID]; + self.session = [FBSession initWithApplication:app]; +} + +- (void)tearDown +{ + [self.session kill]; + [super tearDown]; +} + +- (void)testSettingsAppCanBeOpenedInScopeOfTheCurrentSession +{ + XCUIApplication *testedApp = XCUIApplication.fb_activeApplication; + [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); + XCTAssertEqual([self.session applicationStateWithBundleId:SETTINGS_BUNDLE_ID], 4); + [self.session activateApplicationWithBundleId:testedApp.bundleID]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString: testedApp.bundleID]); + XCTAssertEqual([self.session applicationStateWithBundleId:testedApp.bundleID], 4); +} + +- (void)testSettingsAppCanBeReopenedInScopeOfTheCurrentSession +{ + XCUIApplication *systemApp = self.springboard; + [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); + XCTAssertTrue([self.session terminateApplicationWithBundleId:SETTINGS_BUNDLE_ID]); + FBAssertWaitTillBecomesTrue([systemApp.bundleID isEqualToString:self.session.activeApplication.bundleID]); + [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); +} + +- (void)testMainAppCanBeReactivatedInScopeOfTheCurrentSession +{ + XCUIApplication *testedApp = XCUIApplication.fb_activeApplication; + [self.session launchApplicationWithBundleId:SETTINGS_BUNDLE_ID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:SETTINGS_BUNDLE_ID]); + [self.session activateApplicationWithBundleId:testedApp.bundleID]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:testedApp.bundleID]); +} + +- (void)testMainAppCanBeRestartedInScopeOfTheCurrentSession +{ + XCUIApplication *systemApp = self.springboard; + XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:self.testedApplication.bundleID]; + [self.session terminateApplicationWithBundleId:testedApp.bundleID]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:systemApp.bundleID]); + [self.session launchApplicationWithBundleId:testedApp.bundleID + shouldWaitForQuiescence:nil + arguments:nil + environment:nil]; + FBAssertWaitTillBecomesTrue([self.session.activeApplication.bundleID isEqualToString:testedApp.bundleID]); +} + +- (void)testLaunchUnattachedApp +{ + [FBUnattachedAppLauncher launchAppWithBundleId:SETTINGS_BUNDLE_ID]; + [self.session kill]; + XCTAssertEqualObjects(SETTINGS_BUNDLE_ID, XCUIApplication.fb_activeApplication.bundleID); +} + +- (void)testAppWithInvalidBundleIDCannotBeStarted +{ + XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"yolo"]; + @try { + [testedApp launch]; + XCTFail(@"An exception is expected to be thrown"); + } @catch (NSException *exception) { + XCTAssertEqualObjects(FBApplicationMissingException, exception.name); + } +} + +- (void)testAppWithInvalidBundleIDCannotBeActivated +{ + XCUIApplication *testedApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"yolo"]; + @try { + [testedApp activate]; + XCTFail(@"An exception is expected to be thrown"); + } @catch (NSException *exception) { + XCTAssertEqualObjects(FBApplicationMissingException, exception.name); + } +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBTapTest.m b/WebDriverAgentTests/IntegrationTests/FBTapTest.m new file mode 100644 index 0000000..2d3d2ba --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBTapTest.m @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" + +#import "FBElementCache.h" +#import "FBTestMacros.h" +#import "XCUIDevice+FBRotation.h" +#import "XCUIElement+FBIsVisible.h" + +@interface FBTapTest : FBIntegrationTestCase +@end + +// It is recommnded to verify these tests with different iOS versions + +@implementation FBTapTest + +- (void)verifyTapWithOrientation:(UIDeviceOrientation)orientation +{ + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); +} + +- (void)setUp +{ + // Launch the app everytime to ensure the orientation for each test. + [super setUp]; + [self launchApplication]; + [self goToAlertsPage]; + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + [self resetOrientation]; + [super tearDown]; +} + +- (void)testTap +{ + [self verifyTapWithOrientation:UIDeviceOrientationPortrait]; +} + +- (void)testTapInLandscapeLeft +{ + [self verifyTapWithOrientation:UIDeviceOrientationLandscapeLeft]; +} + +- (void)testTapInLandscapeRight +{ + + [self verifyTapWithOrientation:UIDeviceOrientationLandscapeRight]; +} + +- (void)testTapInPortraitUpsideDown +{ + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } + [self verifyTapWithOrientation:UIDeviceOrientationPortraitUpsideDown]; +} + +- (void)verifyTapByCoordinatesWithOrientation:(UIDeviceOrientation)orientation +{ + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + XCUIElement *dstButton = self.testedApplication.buttons[FBShowAlertButtonName]; + [[dstButton coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] tap]; + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); +} + +- (void)testTapCoordinates +{ + [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortrait]; +} + +- (void)testTapCoordinatesInLandscapeLeft +{ + [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationLandscapeLeft]; +} + +- (void)testTapCoordinatesInLandscapeRight +{ + [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationLandscapeRight]; +} + +- (void)testTapCoordinatesInPortraitUpsideDown +{ + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } + [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortraitUpsideDown]; +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBTestMacros.h b/WebDriverAgentTests/IntegrationTests/FBTestMacros.h new file mode 100644 index 0000000..7e545a7 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBTestMacros.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + Macro used to wait till certain condition is true. + If condition will not become true within default timeout (1m) it will fail running test + */ +#define FBAssertWaitTillBecomesTrue(condition) \ + ({ \ + NSError *__error; \ + XCTAssertTrue([[[FBRunLoopSpinner new] \ + interval:1.0] \ + spinUntilTrue:^BOOL{ \ + return (condition); \ + }]); \ + XCTAssertNil(__error); \ + }) + +#define FBWaitExact(timeoutSeconds) \ + ({ \ + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutSeconds)]]; \ + }) + +#define FBCellElementWithLabel(label) ([self.testedApplication descendantsMatchingType:XCUIElementTypeAny][label]) +#define FBAssertVisibleCell(label) FBAssertWaitTillBecomesTrue(FBCellElementWithLabel(label).fb_isVisible) +#define FBAssertInvisibleCell(label) FBAssertWaitTillBecomesTrue(!FBCellElementWithLabel(label).fb_isVisible) diff --git a/WebDriverAgentTests/IntegrationTests/FBTypingTest.m b/WebDriverAgentTests/IntegrationTests/FBTypingTest.m new file mode 100644 index 0000000..c8a7d0d --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBTypingTest.m @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "XCUIElement.h" +#import "XCUIElement+FBTyping.h" + +@interface FBTypingTest : FBIntegrationTestCase +@end + +@implementation FBTypingTest + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToAttributesPage]; +} + +- (void)testTextTyping +{ + NSString *text = @"Happy typing"; + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + NSError *error; + XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); + XCTAssertNil(error); + XCTAssertEqualObjects(textField.value, text); +} + +- (void)testTextTypingOnFocusedElement +{ + NSString *text = @"Happy typing"; + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + XCTAssertTrue(textField.hasKeyboardFocus); + NSError *error; + XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); + XCTAssertNil(error); + XCTAssertTrue([textField fb_typeText:text shouldClear:NO error:&error]); + XCTAssertNil(error); + NSString *expectedText = [NSString stringWithFormat:@"%@%@", text, text]; + XCTAssertEqualObjects(textField.value, expectedText); +} + +- (void)testTextClearing +{ + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + [textField typeText:@"Happy typing"]; + XCTAssertTrue([textField.value length] > 0); + NSError *error; + XCTAssertTrue([textField fb_clearTextWithError:&error]); + XCTAssertNil(error); + XCTAssertEqualObjects(textField.value, @""); + XCTAssertTrue([textField fb_typeText:@"Happy typing" shouldClear:YES error:&error]); + XCTAssertTrue([textField fb_typeText:@"Happy typing 2" shouldClear:YES error:&error]); + XCTAssertEqualObjects(textField.value, @"Happy typing 2"); + XCTAssertNil(error); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m b/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m new file mode 100644 index 0000000..0068875 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBVideoRecordingTests.m @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" + +#import "FBConfiguration.h" +#import "FBMacros.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" +#import "FBScreenRecordingContainer.h" +#import "FBXCTestDaemonsProxy.h" + +@interface FBVideoRecordingTests : FBIntegrationTestCase +@end + +@implementation FBVideoRecordingTests + +- (void)setUp +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableDiagnosticScreenRecordings"]; + [super setUp]; +} + +- (void)testStartingAndStoppingVideoRecording +{ + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + + // Video recording is only available since iOS 17 + if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { + return; + } + + FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:24 + codec:0]; + NSError *error; + FBScreenRecordingPromise *promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest + error:&error]; + XCTAssertNotNil(promise); + XCTAssertNotNil(promise.identifier); + XCTAssertNil(error); + + [FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise + fps:24 + codec:0]; + XCTAssertEqual(FBScreenRecordingContainer.sharedInstance.screenRecordingPromise, promise); + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.]]; + + BOOL isSuccessfull = [FBXCTestDaemonsProxy stopScreenRecordingWithUUID:promise.identifier error:&error]; + XCTAssertTrue(isSuccessfull); + XCTAssertNil(error); + + [FBScreenRecordingContainer.sharedInstance reset]; + XCTAssertNil(FBScreenRecordingContainer.sharedInstance.screenRecordingPromise); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m new file mode 100644 index 0000000..016c870 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBW3CMultiTouchActionsIntegrationTests.m @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" + +#import "XCUIElement.h" +#import "XCUIApplication+FBTouchAction.h" +#import "FBTestMacros.h" +#import "XCUIDevice+FBRotation.h" +#import "FBRunLoopSpinner.h" + +@interface FBW3CMultiTouchActionsIntegrationTests : FBIntegrationTestCase + +@end + + +@implementation FBW3CMultiTouchActionsIntegrationTests + +- (void)verifyGesture:(NSArray *> *)gesture orientation:(UIDeviceOrientation)orientation +{ + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); +} + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAlertsPage]; + }); + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + [self resetOrientation]; + [super tearDown]; +} + +- (void)testErroneousGestures +{ + NSArray *> *> *invalidGestures = + @[ + // One of the chains has duplicated id + @[ + @{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + @{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + ]; + + for (NSArray *> *invalidGesture in invalidGestures) { + NSError *error; + XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); + XCTAssertNotNil(error); + } +} + +- (void)testSymmetricTwoFingersTap +{ + XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName]; + NSArray *> *gesture = + @[ + @{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + @{ + @"type": @"pointer", + @"id": @"finger2", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + + [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait]; +} + +@end + diff --git a/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m new file mode 100644 index 0000000..c8a12ac --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m @@ -0,0 +1,491 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" + +#import "XCUIElement.h" +#import "XCUIDevice.h" +#import "XCUIApplication+FBTouchAction.h" +#import "FBTestMacros.h" +#import "XCUIDevice+FBRotation.h" +#import "FBRunLoopSpinner.h" +#import "FBXCodeCompatibility.h" + +@interface FBW3CTouchActionsIntegrationTestsPart1 : FBIntegrationTestCase +@end + +@interface FBW3CTouchActionsIntegrationTestsPart2 : FBIntegrationTestCase +@property (nonatomic) XCUIElement *pickerWheel; +@end + + +@implementation FBW3CTouchActionsIntegrationTestsPart1 + +- (void)verifyGesture:(NSArray *> *)gesture orientation:(UIDeviceOrientation)orientation +{ + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); + FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); +} + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAlertsPage]; + }); + [self clearAlert]; +} + +- (void)tearDown +{ + [self clearAlert]; + [self resetOrientation]; + [super tearDown]; +} + +- (void)testErroneousGestures +{ + NSArray *> *> *invalidGestures = + @[ + // Empty chain + @[], + + // Chain element without 'actions' key + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + }, + ], + + // Chain element without type + @[@{ + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element without id + @[@{ + @"type": @"pointer", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element with empty id + @[@{ + @"type": @"pointer", + @"id": @"", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element with unsupported type + @[@{ + @"type": @"key", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element with unsupported pointerType (default) + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element with unsupported pointerType (non-default) + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"pen"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @100, @"y": @100}, + ], + }, + ], + + // Chain element without action item type + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element with singe up action + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element containing action item without y coordinate + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element containing action item with an unknown type + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMoved", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element where action items start with an incorrect item + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element where pointerMove action item does not contain coordinates + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element where pointerMove action item cannot use coordinates of the previous item + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element where action items contains negative duration + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @-100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + // Chain element where action items start with an incorrect one, because the correct one is canceled + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1}, + @{@"type": @"pointerCancel"}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @-100}, + @{@"type": @"pointerUp"}, + ], + }, + ], + + ]; + + for (NSArray *> *invalidGesture in invalidGestures) { + NSError *error; + XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); + XCTAssertNotNil(error); + } +} + +- (void)testNothingDoesWithoutError +{ + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[], + }, + ]; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); + XCTAssertNil(error); +} + +- (void)testTap +{ + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName], @"x": @0, @"y": @0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait]; +} + +- (void)testDoubleTap +{ + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName]}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @50}, + @{@"type": @"pointerUp"}, + @{@"type": @"pause", @"duration": @200}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @50}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; +} + +- (void)testLongPressWithCombinedPause +{ + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName], @"x": @5, @"y": @5}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @200}, + @{@"type": @"pause", @"duration": @200}, + @{@"type": @"pause", @"duration": @100}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeRight]; +} + +- (void)testLongPress +{ + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } + UIDeviceOrientation orientation = UIDeviceOrientationLandscapeLeft; + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; + CGRect elementFrame = self.testedApplication.buttons[FBShowAlertButtonName].frame; + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @(elementFrame.origin.x + 1), @"y": @(elementFrame.origin.y + 1)}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @500}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyGesture:gesture orientation:orientation]; +} + +- (void)testForceTap +{ + if (![XCUIDevice.sharedDevice supportsPressureInteraction]) { + return; + } + + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": self.testedApplication.buttons[FBShowAlertButtonName]}, + @{@"type": @"pointerDown"}, + @{@"type": @"pause", @"duration": @500}, + @{@"type": @"pointerDown", @"pressure": @1.0, @"width":@10, @"height":@10}, + @{@"type": @"pause", @"duration": @50}, + @{@"type": @"pointerDown", @"pressure": @1.0, @"width":@10, @"height":@10}, + @{@"type": @"pause", @"duration": @50}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; +} + +@end + + +@implementation FBW3CTouchActionsIntegrationTestsPart2 + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAttributesPage]; + }); + self.pickerWheel = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; +} + +- (void)tearDown +{ + [self resetOrientation]; + [super tearDown]; +} + +- (void)verifyPickerWheelPositionChangeWithGesture:(NSArray *> *)gesture +{ + NSString *previousValue = self.pickerWheel.value; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]); + XCTAssertNil(error); + XCTAssertTrue([[[[FBRunLoopSpinner new] + timeout:2.0] + timeoutErrorMessage:@"Picker wheel value has not been changed after 2 seconds timeout"] + spinUntilTrue:^BOOL{ + return ![[self.pickerWheel fb_standardSnapshot].value isEqualToString:previousValue]; + } + error:&error]); + XCTAssertNil(error); +} + +- (void)testSwipePickerWheelWithElementCoordinates +{ + CGRect pickerFrame = self.pickerWheel.frame; + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"origin": self.pickerWheel, @"x": @0, @"y":@0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pointerMove", @"duration": @100,@"width":@10, @"height":@10, @"pressure":@0.5, @"origin": self.pickerWheel, @"x": @0, @"y": @(pickerFrame.size.height / 2)}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyPickerWheelPositionChangeWithGesture:gesture]; +} + +- (void)testSwipePickerWheelWithRelativeCoordinates +{ + CGRect pickerFrame = self.pickerWheel.frame; + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @100, @"origin": self.pickerWheel, @"x": @0, @"y": @0}, + @{@"type": @"pointerDown"}, + @{@"type": @"pointerMove", @"duration": @100, @"width":@10, @"height":@10, @"pressure":@0.5, @"origin": @"pointer", @"x": @0, @"y": @(-pickerFrame.size.height / 2)}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyPickerWheelPositionChangeWithGesture:gesture]; +} + +- (void)testSwipePickerWheelWithAbsoluteCoordinates +{ + CGRect pickerFrame = self.pickerWheel.frame; + NSArray *> *gesture = + @[@{ + @"type": @"pointer", + @"id": @"finger1", + @"parameters": @{@"pointerType": @"touch"}, + @"actions": @[ + @{@"type": @"pointerMove", @"duration": @0, @"x": @(pickerFrame.origin.x + pickerFrame.size.width / 2), @"y": @(pickerFrame.origin.y + pickerFrame.size.height / 2)}, + @{@"type": @"pointerDown"}, + @{@"type": @"pointerMove", @"duration": @100,@"width":@10, @"height":@10, @"pressure":@0.5, @"origin": @"pointer", @"x": @0, @"y": @(pickerFrame.size.height / 2)}, + @{@"type": @"pointerUp"}, + ], + }, + ]; + [self verifyPickerWheelPositionChangeWithGesture:gesture]; +} + +@end + + diff --git a/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m b/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m new file mode 100644 index 0000000..84a5c6c --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBW3CTypeActionsTests.m @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "XCUIElement.h" +#import "XCUIElement+FBTyping.h" +#import "XCUIApplication+FBTouchAction.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "FBRuntimeUtils.h" +#import "FBXCodeCompatibility.h" + + +@interface FBW3CTypeActionsTests : FBIntegrationTestCase +@end + +@implementation FBW3CTypeActionsTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToAttributesPage]; +} + +- (void)testErroneousGestures +{ + if (![XCPointerEvent.class fb_areKeyEventsSupported]) { + return; + } + + NSArray *> *> *invalidGestures = + @[ + + // missing balance 1 + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"keyDown", @"value": @"h"}, + @{@"type": @"keyUp", @"value": @"k"}, + ], + }, + ], + + // missing balance 2 + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"keyDown", @"value": @"h"}, + ], + }, + ], + + // missing balance 3 + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"keyUp", @"value": @"h"}, + ], + }, + ], + + // missing key value + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"keyUp"}, + ], + }, + ], + + // wrong key value + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"keyUp", @"value": @500}, + ], + }, + ], + + // missing duration value + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"pause"}, + ], + }, + ], + + // wrong duration value + @[@{ + @"type": @"key", + @"id": @"keyboard", + @"actions": @[ + @{@"type": @"duration", @"duration": @"bla"}, + ], + }, + ], + + ]; + + for (NSArray *> *invalidGesture in invalidGestures) { + NSError *error; + XCTAssertFalse([self.testedApplication fb_performW3CActions:invalidGesture elementCache:nil error:&error]); + XCTAssertNotNil(error); + } +} + +- (void)testTextTyping +{ + if (![XCPointerEvent.class fb_areKeyEventsSupported]) { + return; + } + + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + NSArray *> *typeAction = + @[ + @{ + @"type": @"key", + @"id": @"keyboard2", + @"actions": @[ + @{@"type": @"pause", @"duration": @500}, + @{@"type": @"keyDown", @"value": @"🏀"}, + @{@"type": @"keyUp", @"value": @"🏀"}, + @{@"type": @"keyDown", @"value": @"N"}, + @{@"type": @"keyUp", @"value": @"N"}, + @{@"type": @"keyDown", @"value": @"B"}, + @{@"type": @"keyUp", @"value": @"B"}, + @{@"type": @"keyDown", @"value": @"A"}, + @{@"type": @"keyUp", @"value": @"A"}, + @{@"type": @"keyDown", @"value": @"a"}, + @{@"type": @"keyUp", @"value": @"a"}, + @{@"type": @"keyDown", @"value": [NSString stringWithFormat:@"%C", 0xE003]}, + @{@"type": @"keyUp", @"value": [NSString stringWithFormat:@"%C", 0xE003]}, + @{@"type": @"pause", @"duration": @500}, + ], + }, + ]; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:typeAction + elementCache:nil + error:&error]); + XCTAssertNil(error); + XCTAssertEqualObjects(textField.wdValue, @"🏀NBA"); +} + +- (void)testTextTypingWithEmptyActions +{ + if (![XCPointerEvent.class fb_areKeyEventsSupported]) { + return; + } + + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + NSArray *> *typeAction = + @[ + @{ + @"type": @"pointer", + @"id": @"touch", + @"actions": @[], + }, + ]; + NSError *error; + XCTAssertTrue([self.testedApplication fb_performW3CActions:typeAction + elementCache:nil + error:&error]); + XCTAssertNil(error); + XCTAssertEqualObjects(textField.value, @""); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m new file mode 100644 index 0000000..7082224 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBMacros.h" +#import "FBTestMacros.h" +#import "FBXPath.h" +#import "FBXCAccessibilityElement.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXMLGenerationOptions.h" +#import "XCUIApplication.h" +#import "XCUIElement.h" +#import "XCUIElement+FBFind.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" + + +@interface FBXPathIntegrationTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *testedView; +@end + +@implementation FBXPathIntegrationTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); + self.testedView = self.testedApplication.otherElements[@"MainView"]; + XCTAssertTrue(self.testedView.exists); + FBAssertWaitTillBecomesTrue(self.testedView.buttons.count > 0); +} + +- (id)destinationSnapshot +{ + XCUIElement *matchingElement = self.testedView.buttons.allElementsBoundByIndex.firstObject; + id snapshot = [matchingElement fb_customSnapshot]; + // Over iOS13, snapshot returns a child. + // The purpose of here is return a single element to replace children with an empty array for testing. + snapshot.children = @[]; + return snapshot; +} + +- (void)testApplicationNodeXMLRepresentation +{ + id snapshot = [self.testedApplication fb_customSnapshot]; + snapshot.children = @[]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot + options:nil]; + int pid = [snapshot.accessibilityElement processIdentifier]; + XCTAssertNotNil(xmlStr); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\" processId=\"%d\" bundleId=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits, pid, [self.testedApplication bundleID]]; + XCTAssertEqualObjects(xmlStr, expectedXml); +} + +- (void)testSingleDescendantXMLRepresentation +{ + id snapshot = self.destinationSnapshot; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot + options:nil]; + XCTAssertNotNil(xmlStr); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits]; + XCTAssertEqualObjects(xmlStr, expectedXml); +} + +- (void)testSingleDescendantXMLRepresentationWithScope +{ + id snapshot = self.destinationSnapshot; + NSString *scope = @"AppiumAUT"; + FBXMLGenerationOptions *options = [[FBXMLGenerationOptions new] withScope:scope]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot + options:options]; + XCTAssertNotNil(xmlStr); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@>\n <%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\"/>\n\n", scope, wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, wrappedSnapshot.wdTraits, scope]; + XCTAssertEqualObjects(xmlStr, expectedXml); +} + +- (void)testSingleDescendantXMLRepresentationWithoutAttributes +{ + id snapshot = self.destinationSnapshot; + FBXMLGenerationOptions *options = [[FBXMLGenerationOptions new] + withExcludedAttributes:@[@"visible", @"enabled", @"index", @"blabla"]]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot + options:options]; + XCTAssertNotNil(xmlStr); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" traits=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdTraits]; + XCTAssertEqualObjects(xmlStr, expectedXml); +} + +- (void)testFindMatchesInElement +{ + NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@"//XCUIElementTypeButton"]; + XCTAssertEqual([matchingSnapshots count], 5); + for (id element in matchingSnapshots) { + XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]); + } +} + +- (void)testFindMatchesWithoutContextScopeLimit +{ + XCUIElement *button = self.testedApplication.buttons.firstMatch; + BOOL previousValue = FBConfiguration.limitXpathContextScope; + FBConfiguration.limitXpathContextScope = NO; + @try { + NSArray *parentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@".."]; + XCTAssertEqual(parentSnapshots.count, 1); + XCTAssertEqualObjects( + [FBXCElementSnapshotWrapper ensureWrapped:[parentSnapshots objectAtIndex:0]].wdLabel, + @"MainView" + ); + NSArray *elements = [button.application fb_filterDescendantsWithSnapshots:parentSnapshots onlyChildren:NO]; + XCTAssertEqual(elements.count, 1); + XCTAssertEqualObjects( + [[elements objectAtIndex:0] wdLabel], + @"MainView" + ); + NSArray *currentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@"."]; + XCTAssertEqual(currentSnapshots.count, 1); + XCTAssertEqualObjects( + [FBXCElementSnapshotWrapper ensureWrapped:[currentSnapshots objectAtIndex:0]].wdType, + @"XCUIElementTypeButton" + ); + NSArray *currentElements = [button.application fb_filterDescendantsWithSnapshots:currentSnapshots onlyChildren:NO]; + XCTAssertEqual(currentElements.count, 1); + XCTAssertEqualObjects( + [[currentElements objectAtIndex:0] wdType], + @"XCUIElementTypeButton" + ); + } @finally { + FBConfiguration.limitXpathContextScope = previousValue; + } +} + +- (void)testFindMatchesInElementWithDotNotation +{ + NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@".//XCUIElementTypeButton"]; + XCTAssertEqual([matchingSnapshots count], 5); + for (id element in matchingSnapshots) { + XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]); + } +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/Info.plist b/WebDriverAgentTests/IntegrationTests/Info.plist new file mode 100644 index 0000000..6104b22 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.wda.integrationTests + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m new file mode 100644 index 0000000..3258f6b --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "XCUIElement.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" + +@interface XCElementSnapshotHelperTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *testedView; +@end + +@implementation XCElementSnapshotHelperTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); + self.testedView = self.testedApplication.otherElements[@"MainView"]; + XCTAssertTrue(self.testedView.exists); +} + +- (void)testDescendantsMatchingType +{ + NSSet *expectedLabels = [NSSet setWithArray:@[ + @"Alerts", + @"Attributes", + @"Scrolling", + @"Deadlock app", + @"Touch", + ]]; + NSArray> *matchingSnapshots = [[FBXCElementSnapshotWrapper ensureWrapped: + [self.testedView fb_customSnapshot]] + fb_descendantsMatchingType:XCUIElementTypeButton]; + XCTAssertEqual(matchingSnapshots.count, expectedLabels.count); + NSArray *labels = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.label"]; + XCTAssertEqualObjects([NSSet setWithArray:labels], expectedLabels); + + NSArray *types = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.elementType"]; + XCTAssertEqual(types.count, 1, @"matchingSnapshots should contain only one type"); + XCTAssertEqualObjects(types.lastObject, @(XCUIElementTypeButton), @"matchingSnapshots should contain only one type"); +} + +- (void)testParentMatchingType +{ + XCUIElement *button = self.testedApplication.buttons[@"Alerts"]; + FBAssertWaitTillBecomesTrue(button.exists); + id windowSnapshot = [[FBXCElementSnapshotWrapper ensureWrapped: + [self.testedView fb_customSnapshot]] + fb_parentMatchingType:XCUIElementTypeWindow]; + XCTAssertNotNil(windowSnapshot); + XCTAssertEqual(windowSnapshot.elementType, XCUIElementTypeWindow); +} + +@end + +@interface XCElementSnapshotHelperTests_AttributePage : FBIntegrationTestCase +@end + +@implementation XCElementSnapshotHelperTests_AttributePage + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAttributesPage]; + }); +} + +- (void)testParentMatchingOneOfTypes +{ + XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; + FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); + id datePicker = [[FBXCElementSnapshotWrapper ensureWrapped: + [todayPickerWheel fb_customSnapshot]] + fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeDatePicker), @(XCUIElementTypeWindow)]]; + XCTAssertNotNil(datePicker); + XCTAssertEqual(datePicker.elementType, XCUIElementTypeDatePicker); +} + +- (void)testParentMatchingOneOfTypesWithXCUIElementTypeAny +{ + XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; + FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); + id otherSnapshot =[[FBXCElementSnapshotWrapper ensureWrapped: + [todayPickerWheel fb_customSnapshot]] + fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeAny)]]; + XCTAssertNotNil(otherSnapshot); +} + +- (void)testParentMatchingOneOfTypesWithAbsentParents +{ + XCUIElement *todayPickerWheel = self.testedApplication.pickerWheels[@"Today"]; + FBAssertWaitTillBecomesTrue(todayPickerWheel.exists); + id otherSnapshot = [[FBXCElementSnapshotWrapper ensureWrapped: + [todayPickerWheel fb_customSnapshot]] + fb_parentMatchingOneOfTypes:@[@(XCUIElementTypeTab), @(XCUIElementTypeLink)]]; + XCTAssertNil(otherSnapshot); +} + +@end + +@interface XCElementSnapshotHelperTests_ScrollView : FBIntegrationTestCase +@end + +@implementation XCElementSnapshotHelperTests_ScrollView + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToScrollPageWithCells:false]; + }); +} + +- (void)testParentMatchingOneOfTypesWithFilter +{ + XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; + FBAssertWaitTillBecomesTrue(threeStaticText.exists); + NSArray *acceptedParents = @[ + @(XCUIElementTypeScrollView), + @(XCUIElementTypeCollectionView), + @(XCUIElementTypeTable), + ]; + id scrollView = [[FBXCElementSnapshotWrapper ensureWrapped: + [threeStaticText fb_customSnapshot]] + fb_parentMatchingOneOfTypes:acceptedParents + filter:^BOOL(id snapshot) { + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] isWDVisible]; + }]; + XCTAssertEqualObjects(scrollView.identifier, @"scrollView"); +} + +- (void)testParentMatchingOneOfTypesWithFilterRetruningNo +{ + XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; + FBAssertWaitTillBecomesTrue(threeStaticText.exists); + NSArray *acceptedParents = @[ + @(XCUIElementTypeScrollView), + @(XCUIElementTypeCollectionView), + @(XCUIElementTypeTable), + ]; + id scrollView = [[FBXCElementSnapshotWrapper ensureWrapped: + [threeStaticText fb_customSnapshot]] + fb_parentMatchingOneOfTypes:acceptedParents + filter:^BOOL(id snapshot) { + return NO; + }]; + XCTAssertNil(scrollView); +} + +- (void)testDescendantsCellSnapshots +{ + XCUIElement *scrollView = self.testedApplication.scrollViews[@"scrollView"]; + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); + NSArray *cells = [[FBXCElementSnapshotWrapper ensureWrapped: + [scrollView fb_customSnapshot]] + fb_descendantsCellSnapshots]; + XCTAssertGreaterThanOrEqual(cells.count, 10); + id element = cells.firstObject; + XCTAssertEqualObjects(element.label, @"0"); +} + +@end + +@interface XCElementSnapshotHelperTests_ScrollViewCells : FBIntegrationTestCase +@end + +@implementation XCElementSnapshotHelperTests_ScrollViewCells + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToScrollPageWithCells:true]; + }); +} + +- (void)testParentCellSnapshot +{ + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible); + XCUIElement *threeStaticText = self.testedApplication.staticTexts[@"3"]; + id xcuiElementCell = [[FBXCElementSnapshotWrapper ensureWrapped: + [threeStaticText fb_customSnapshot]] + fb_parentCellSnapshot]; + XCTAssertEqual(xcuiElementCell.elementType, 75); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m b/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m new file mode 100644 index 0000000..453d0ae --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHitPointTests.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement.h" +#import "XCUIElement+FBUtilities.h" + +@interface XCElementSnapshotHitPoint : FBIntegrationTestCase +@end + +@implementation XCElementSnapshotHitPoint + +- (void)testAccessibilityActivationPoint +{ + [self launchApplication]; + [self goToAttributesPage]; + XCUIElement *dstBtn = self.testedApplication.buttons[@"not_accessible"]; + CGPoint hitPoint = [FBXCElementSnapshotWrapper + ensureWrapped:[dstBtn fb_standardSnapshot]].fb_hitPoint.CGPointValue; + XCTAssertTrue(hitPoint.x > 0 && hitPoint.y > 0); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m new file mode 100644 index 0000000..3d78a0d --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +#import "FBIntegrationTestCase.h" +#import "FBElement.h" +#import "FBMacros.h" +#import "FBTestMacros.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIElement+FBIsVisible.h" +#import "FBXCodeCompatibility.h" + +void calculateMaxTreeDepth(NSDictionary *tree, NSNumber *currentDepth, NSNumber** maxDepth) { + if (nil == maxDepth) { + return; + } + + NSArray *children = tree[@"children"]; + if (nil == children || 0 == children.count) { + return; + } + for (NSDictionary *child in children) { + if (currentDepth.integerValue > [*maxDepth integerValue]) { + *maxDepth = currentDepth; + } + calculateMaxTreeDepth(child, @(currentDepth.integerValue + 1), maxDepth); + } +} + +@interface XCUIApplicationHelperTests : FBIntegrationTestCase +@end + +@implementation XCUIApplicationHelperTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; +} + +- (void)testQueringSpringboard +{ + [self goToSpringBoardFirstPage]; + XCTAssertTrue(XCUIApplication.fb_systemApplication.icons[@"Safari"].exists); + XCTAssertTrue(XCUIApplication.fb_systemApplication.icons[@"Calendar"].firstMatch.exists); +} + +- (void)testApplicationTree +{ + NSDictionary *tree = self.testedApplication.fb_tree; + XCTAssertNotNil(tree); + NSNumber *maxDepth; + calculateMaxTreeDepth(tree, @0, &maxDepth); + XCTAssertGreaterThan(maxDepth.integerValue, 3); + XCTAssertNotNil(self.testedApplication.fb_accessibilityTree); +} + +- (void)testApplicationTreeAttributesFiltering +{ + NSDictionary *applicationTree = [self.testedApplication fb_tree:[NSSet setWithArray:@[@"visible"]]]; + XCTAssertNotNil(applicationTree); + XCTAssertNil([applicationTree objectForKey:@"isVisible"], @"'isVisible' key should not be present in the application tree"); +} + +- (void)testDeactivateApplication +{ + NSError *error; + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); + NSTimeInterval backgroundDuration = 5.0; + XCTAssertTrue([self.testedApplication fb_deactivateWithDuration:backgroundDuration error:&error]); + NSTimeInterval timeElapsed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted) / NSEC_PER_SEC; + XCTAssertNil(error); + XCTAssertEqualWithAccuracy(timeElapsed, backgroundDuration, 3.0); + XCTAssertTrue(self.testedApplication.buttons[@"Alerts"].exists); +} + +- (void)testActiveApplication +{ + XCUIApplication *systemApp = XCUIApplication.fb_systemApplication; + XCTAssertTrue([XCUIApplication fb_activeApplication].buttons[@"Alerts"].fb_isVisible); + [self goToSpringBoardFirstPage]; + XCTAssertEqualObjects([XCUIApplication fb_activeApplication].bundleID, systemApp.bundleID); + XCTAssertTrue(systemApp.icons[@"Safari"].fb_isVisible); +} + +- (void)testActiveElement +{ + [self goToAttributesPage]; + XCTAssertNil(self.testedApplication.fb_activeElement); + XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; + [textField tap]; + FBAssertWaitTillBecomesTrue(nil != self.testedApplication.fb_activeElement); + XCTAssertEqualObjects(((id)self.testedApplication.fb_activeElement).wdUID, + ((id)textField).wdUID); +} + +- (void)testActiveApplicationsInfo +{ + NSArray *appsInfo = [XCUIApplication fb_activeAppsInfo]; + XCTAssertTrue(appsInfo.count > 0); + BOOL isAppActive = NO; + for (NSDictionary *appInfo in appsInfo) { + if ([appInfo[@"bundleId"] isEqualToString:self.testedApplication.bundleID]) { + isAppActive = YES; + break; + } + } + XCTAssertTrue(isAppActive); +} + +- (void)testTestmanagerdVersion +{ + XCTAssertGreaterThan(FBTestmanagerdVersion(), 0); +} + +- (void)testAccessbilityAudit +{ + if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { + return; + } + + NSError *error; + NSArray *auditIssues1 = [XCUIApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypes:~0UL + error:&error]; + XCTAssertNotNil(auditIssues1); + XCTAssertNil(error); + + NSMutableSet *set = [NSMutableSet new]; + [set addObject:@"XCUIAccessibilityAuditTypeAll"]; + NSArray *auditIssues2 = [XCUIApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypesSet:set.copy + error:&error]; + // 'elementDescription' is not in this list because it could have + // different object id's debug description in XCTest. + NSArray *checkKeys = @[ + @"auditType", + @"compactDescription", + @"detailedDescription", + @"element", + @"elementAttributes" + ]; + + XCTAssertEqual([auditIssues1 count], [auditIssues2 count]); + for (int i = 1; i < [auditIssues1 count]; i++) { + for (NSString *k in checkKeys) { + XCTAssertEqualObjects( + [auditIssues1[i] objectForKey:k], + [auditIssues2[i] objectForKey:k] + ); + } + } + XCTAssertNil(error); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m new file mode 100644 index 0000000..dfbd6f8 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHealthCheckTests.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "XCUIDevice+FBHealthCheck.h" +#import "XCUIElement.h" + +@interface XCUIDeviceHealthCheckTests : FBIntegrationTestCase +@end + +@implementation XCUIDeviceHealthCheckTests + +- (void)testHealthCheck +{ + [self launchApplication]; + XCTAssertTrue(self.testedApplication.exists); + XCTAssertTrue([[XCUIDevice sharedDevice] fb_healthCheckWithApplication:self.testedApplication]); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m new file mode 100644 index 0000000..97908ad --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBImageUtils.h" +#import "FBMacros.h" +#import "FBTestMacros.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice+FBHelpers.h" +#import "XCUIDevice+FBRotation.h" +#import "XCUIScreen.h" + +@interface XCUIDeviceHelperTests : FBIntegrationTestCase +@end + +@implementation XCUIDeviceHelperTests + +- (void)restorePortraitOrientation +{ + if ([XCUIDevice sharedDevice].orientation != UIDeviceOrientationPortrait) { + [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationPortrait]; + } +} + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self restorePortraitOrientation]; +} + +- (void)tearDown +{ + [self restorePortraitOrientation]; + [super tearDown]; +} + +- (void)testScreenshot +{ + NSError *error = nil; + NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error]; + XCTAssertNotNil(screenshotData); + XCTAssertNil(error); + XCTAssertTrue(FBIsPngImage(screenshotData)); + + UIImage *screenshot = [UIImage imageWithData:screenshotData]; + XCTAssertNotNil(screenshot); + + XCUIScreen *mainScreen = XCUIScreen.mainScreen; + UIImage *screenshotExact = ((XCUIScreenshot *)mainScreen.screenshot).image; + XCTAssertEqualWithAccuracy(screenshotExact.size.height * mainScreen.scale, + screenshot.size.height, + FLT_EPSILON); + XCTAssertEqualWithAccuracy(screenshotExact.size.width * mainScreen.scale, + screenshot.size.width, + FLT_EPSILON); +} + +- (void)testLandscapeScreenshot +{ + XCTAssertTrue([[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft]); + NSError *error = nil; + NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error]; + XCTAssertNotNil(screenshotData); + XCTAssertTrue(FBIsPngImage(screenshotData)); + XCTAssertNil(error); + + UIImage *screenshot = [UIImage imageWithData:screenshotData]; + XCTAssertNotNil(screenshot); + XCTAssertTrue(screenshot.size.width > screenshot.size.height); + + XCUIScreen *mainScreen = XCUIScreen.mainScreen; + UIImage *screenshotExact = ((XCUIScreenshot *)mainScreen.screenshot).image; + CGSize realMainScreenSize = screenshotExact.size.height > screenshot.size.width + ? CGSizeMake(screenshotExact.size.height * mainScreen.scale, screenshotExact.size.width * mainScreen.scale) + : CGSizeMake(screenshotExact.size.width * mainScreen.scale, screenshotExact.size.height * mainScreen.scale); + XCTAssertEqualWithAccuracy(realMainScreenSize.height, screenshot.size.height, FLT_EPSILON); + XCTAssertEqualWithAccuracy(realMainScreenSize.width, screenshot.size.width, FLT_EPSILON); +} + +- (void)testWifiAddress +{ + NSString *adderss = [XCUIDevice sharedDevice].fb_wifiIPAddress; + if (!adderss) { + return; + } + NSRange range = [adderss rangeOfString:@"^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})" options:NSRegularExpressionSearch]; + XCTAssertTrue(range.location != NSNotFound); +} + +- (void)testGoToHomeScreen +{ + NSError *error; + XCTAssertTrue([[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]); + XCTAssertNil(error); + FBAssertWaitTillBecomesTrue([XCUIApplication fb_activeApplication].icons[@"Safari"].exists); +} + +- (void)testLockUnlockScreen +{ + XCTAssertFalse([[XCUIDevice sharedDevice] fb_isScreenLocked]); + NSError *error; + XCTAssertTrue([[XCUIDevice sharedDevice] fb_lockScreen:&error]); + XCTAssertTrue([[XCUIDevice sharedDevice] fb_isScreenLocked]); + XCTAssertNil(error); + XCTAssertTrue([[XCUIDevice sharedDevice] fb_unlockScreen:&error]); + XCTAssertFalse([[XCUIDevice sharedDevice] fb_isScreenLocked]); + XCTAssertNil(error); +} + +- (void)testUrlSchemeActivation +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" error:&error]); + FBAssertWaitTillBecomesTrue([XCUIApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); + XCTAssertNil(error); +} + +- (void)testUrlSchemeActivationWithApp +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" + withApplication:@"com.apple.mobilesafari" + error:&error]); + FBAssertWaitTillBecomesTrue([XCUIApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); + XCTAssertNil(error); +} + +#if !TARGET_OS_TV +- (void)testSimulatedLocationSetup +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + + CLLocation *simulatedLocation = [[CLLocation alloc] initWithLatitude:50 longitude:50]; + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_setSimulatedLocation:simulatedLocation error:&error]); + XCTAssertNil(error); + CLLocation *currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + XCTAssertNil(error); + XCTAssertNotNil(currentLocation); + XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); + XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); + XCTAssertTrue([XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]); + XCTAssertNil(error); + currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + XCTAssertNil(error); + XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); + XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); +} +#endif + +- (void)testPressingUnsupportedButton +{ + NSError *error; + NSNumber *duration = nil; + XCTAssertFalse([XCUIDevice.sharedDevice fb_pressButton:@"volumeUpp" + forDuration:duration + error:&error]); + XCTAssertNotNil(error); +} + +- (void)testPressingSupportedButton +{ + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_pressButton:@"home" + forDuration:nil + error:&error]); + XCTAssertNil(error); +} + +- (void)testPressingSupportedButtonNumber +{ + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_pressButton:@"home" + forDuration:[NSNumber numberWithDouble:1.0] + error:&error]); + XCTAssertNil(error); +} + +- (void)testLongPressHomeButton +{ + NSError *error; + // kHIDPage_Consumer = 0x0C + // kHIDUsage_Csmr_Menu = 0x40 + XCTAssertTrue([XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:0x0C + usage:0x40 + duration:1.0 + error:&error]); + XCTAssertNil(error); +} + +- (void)testAppearance +{ + if (SYSTEM_VERSION_LESS_THAN(@"15.0")) { + return; + } + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_setAppearance:FBUIInterfaceAppearanceDark error:&error]); + XCTAssertNil(error); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m b/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m new file mode 100644 index 0000000..6087980 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import "FBIntegrationTestCase.h" +#import "XCUIDevice+FBRotation.h" + +@interface XCUIDeviceRotationTests : FBIntegrationTestCase + +@end + +@implementation XCUIDeviceRotationTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; +} + +- (void)tearDown +{ + [self resetOrientation]; + [super tearDown]; +} + +- (void)testLandscapeRightOrientation +{ + BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeRight]; + XCTAssertTrue(success, @"Device should support LandscapeRight"); + // Device rotation gives opposite interface rotation + XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeLeft"].exists); +} + +- (void)testLandscapeLeftOrientation +{ + BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft]; + XCTAssertTrue(success, @"Device should support LandscapeLeft"); + // Device rotation gives opposite interface rotation + XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeRight"].exists); +} + +- (void)testLandscapeRightRotation +{ + BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ + @"x" : @(0), + @"y" : @(0), + @"z" : @(90) + }]; + XCTAssertTrue(success, @"Device should support LandscapeRight"); + // Device rotation gives opposite interface rotation + XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeLeft"].exists); +} + +- (void)testLandscapeLeftRotation +{ + BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ + @"x" : @(0), + @"y" : @(0), + @"z" : @(270) + }]; + XCTAssertTrue(success, @"Device should support LandscapeLeft"); + // Device rotation gives opposite interface rotation + XCTAssertTrue(self.testedApplication.staticTexts[@"LandscapeRight"].exists); +} + +- (void)testRotationTiltRotation +{ + UIDeviceOrientation currentRotation = [XCUIDevice sharedDevice].orientation; + BOOL success = [[XCUIDevice sharedDevice] fb_setDeviceRotation:@{ + @"x" : @(15), + @"y" : @(0), + @"z" : @(0)} + ]; + XCTAssertFalse(success, @"Device should not support tilt"); + XCTAssertEqual(currentRotation, [XCUIDevice sharedDevice].orientation, @"Device doesnt support tilt, should be at previous orientation"); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m b/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m new file mode 100644 index 0000000..26773b3 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIElementAttributesTests.m @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "XCUIElement+FBWebDriverAttributes.h" +#import "XCUIElement+FBFind.h" +#import "FBElementUtils.h" +#import "FBConfiguration.h" +#import "FBResponsePayload.h" +#import "FBXCodeCompatibility.h" + +@interface XCUIElementAttributesTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *matchingElement; +@end + +@implementation XCUIElementAttributesTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); + XCUIElement *testedView = self.testedApplication.otherElements[@"MainView"]; + FBAssertWaitTillBecomesTrue(testedView.exists); + self.matchingElement = [[testedView fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:YES] firstObject]; + XCTAssertNotNil(self.matchingElement); +} + +- (void)verifyGettingAttributeWithShortcut:(NSString *)shortcutName expectedValue:(id)expectedValue +{ + NSString *fullAttributeName = [NSString stringWithFormat:@"wd%@", [NSString stringWithFormat:@"%@%@", [[shortcutName substringToIndex:1] uppercaseString], [shortcutName substringFromIndex:1]]]; + id actualValue = [self.matchingElement fb_valueForWDAttributeName:fullAttributeName]; + id actualShortcutValue = [self.matchingElement fb_valueForWDAttributeName:shortcutName]; + if (nil == expectedValue) { + XCTAssertNil(actualValue); + XCTAssertNil(actualShortcutValue); + return; + } + if ([actualValue isKindOfClass:NSString.class]) { + XCTAssertTrue([actualValue isEqualToString:expectedValue]); + XCTAssertTrue([actualShortcutValue isEqualToString:expectedValue]); + } else if ([actualValue isKindOfClass:NSNumber.class]) { + XCTAssertTrue([actualValue isEqualToNumber:expectedValue]); + XCTAssertTrue([actualShortcutValue isEqualToNumber:expectedValue]); + } else { + XCTAssertEqual(actualValue, expectedValue); + XCTAssertEqual(actualShortcutValue, expectedValue); + } +} + +- (void)testGetNameAttribute +{ + [self verifyGettingAttributeWithShortcut:@"name" expectedValue:self.matchingElement.wdName]; +} + +- (void)testGetValueAttribute +{ + [self verifyGettingAttributeWithShortcut:@"value" expectedValue:self.matchingElement.wdValue]; +} + +- (void)testGetLabelAttribute +{ + [self verifyGettingAttributeWithShortcut:@"label" expectedValue:self.matchingElement.wdLabel]; +} + +- (void)testGetTypeAttribute +{ + [self verifyGettingAttributeWithShortcut:@"type" expectedValue:self.matchingElement.wdType]; +} + +- (void)testGetRectAttribute +{ + NSString *shortcutName = @"rect"; + for (NSString *key in @[@"x", @"y", @"width", @"height"]) { + NSNumber *actualValue = [self.matchingElement fb_valueForWDAttributeName:[FBElementUtils wdAttributeNameForAttributeName:shortcutName]][key]; + NSNumber *actualShortcutValue = [self.matchingElement fb_valueForWDAttributeName:shortcutName][key]; + NSNumber *expectedValue = self.matchingElement.wdRect[key]; + XCTAssertTrue([actualValue isEqualToNumber:expectedValue]); + XCTAssertTrue([actualShortcutValue isEqualToNumber:expectedValue]); + } +} + +- (void)testGetEnabledAttribute +{ + [self verifyGettingAttributeWithShortcut:@"enabled" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdEnabled]]; +} + +- (void)testGetAccessibleAttribute +{ + [self verifyGettingAttributeWithShortcut:@"accessible" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdAccessible]]; +} + +- (void)testGetUidAttribute +{ + [self verifyGettingAttributeWithShortcut:@"UID" expectedValue:self.matchingElement.wdUID]; +} + +- (void)testGetVisibleAttribute +{ + [self verifyGettingAttributeWithShortcut:@"visible" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdVisible]]; +} + +- (void)testGetAccessibilityContainerAttribute +{ + [self verifyGettingAttributeWithShortcut:@"accessibilityContainer" expectedValue:[NSNumber numberWithBool:self.matchingElement.wdAccessibilityContainer]]; +} + +- (void)testGetInvalidAttribute +{ + XCTAssertThrowsSpecificNamed([self verifyGettingAttributeWithShortcut:@"invalid" expectedValue:@"blabla"], NSException, FBUnknownAttributeException); +} + +@end + +@interface XCUIElementFBFindTests_CompactResponses : FBIntegrationTestCase +@end + + +@implementation XCUIElementFBFindTests_CompactResponses + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); +} + +- (void)testCompactResponseYes +{ + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, YES); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqual(fields.count, 2); +} + +- (void)testCompactResponseNo +{ + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqualObjects(fields[@"type"], @"XCUIElementTypeButton"); + XCTAssertEqualObjects(fields[@"label"], @"Alerts"); + XCTAssertEqual(fields.count, 4); +} + +@end + + +@interface XCUIElementFBFindTests_ResponseFields : FBIntegrationTestCase +@end + +@implementation XCUIElementFBFindTests_ResponseFields + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); +} + +- (void)testCompactResponseYesWithResponseAttributesSet +{ + [FBConfiguration setElementResponseAttributes:@"name,text,enabled"]; + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, YES); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqual(fields.count, 2); +} + +- (void)testCompactResponseNoWithResponseAttributesSet +{ + [FBConfiguration setElementResponseAttributes:@"name,text,enabled"]; + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); + XCTAssertEqualObjects(fields[@"text"], @"Alerts"); + XCTAssertEqualObjects(fields[@"enabled"], @(YES)); + XCTAssertEqual(fields.count, 5); +} + +- (void)testInvalidAttribute +{ + [FBConfiguration setElementResponseAttributes:@"invalid_field,name"]; + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); + XCTAssertEqual(fields.count, 3); +} + +- (void)testKnownAttributes +{ + [FBConfiguration setElementResponseAttributes:@"name,type,label,text,rect,enabled,displayed,selected"]; + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqualObjects(fields[@"name"], @"XCUIElementTypeButton"); + XCTAssertEqualObjects(fields[@"type"], @"XCUIElementTypeButton"); + XCTAssertEqualObjects(fields[@"label"], @"Alerts"); + XCTAssertEqualObjects(fields[@"text"], @"Alerts"); + XCTAssertTrue(matchesRegex([fields[@"rect"] description], @"\\{\\s*height = [0-9]+;\\s*width = [0-9]+;\\s*x = [0-9]+;\\s*y = [0-9]+;\\s*\\}")); + XCTAssertEqualObjects(fields[@"enabled"], @(YES)); + XCTAssertEqualObjects(fields[@"displayed"], @(YES)); + XCTAssertEqualObjects(fields[@"selected"], @(NO)); + XCTAssertEqual(fields.count, 10); +} + +- (void)testArbitraryAttributes +{ + [FBConfiguration setElementResponseAttributes:@"attribute/name,attribute/value"]; + XCUIElement *alertsButton = self.testedApplication.buttons[@"Alerts"]; + NSDictionary *fields = FBDictionaryResponseWithElement(alertsButton, NO); + XCTAssertNotNil(fields[@"ELEMENT"]); + XCTAssertNotNil(fields[@"element-6066-11e4-a52e-4f735466cecf"]); + XCTAssertEqualObjects(fields[@"attribute/name"], @"Alerts"); + XCTAssertEqualObjects(fields[@"attribute/value"], [NSNull null]); + XCTAssertEqual(fields.count, 4); +} + +static BOOL matchesRegex(NSString *target, NSString *pattern) { + if (!target) + return NO; + NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL]; + return [regex numberOfMatchesInString:target options:0 range:NSMakeRange(0, target.length)] == 1; +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m b/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m new file mode 100644 index 0000000..296c891 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m @@ -0,0 +1,472 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBElementUtils.h" +#import "FBExceptions.h" +#import "FBTestMacros.h" +#import "XCUIElement.h" +#import "XCUIElement+FBFind.h" +#import "XCUIElement+FBUID.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBClassChain.h" +#import "XCUIElement+FBResolve.h" +#import "FBXPath.h" +#import "FBXCodeCompatibility.h" + +@interface XCUIElementFBFindTests : FBIntegrationTestCase +@property (nonatomic, strong) XCUIElement *testedView; +@end + +@implementation XCUIElementFBFindTests + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + }); + self.testedView = self.testedApplication.otherElements[@"MainView"]; + FBAssertWaitTillBecomesTrue(self.testedView.exists); +} + +- (void)testDescendantsWithClassName +{ + NSSet *expectedLabels = [NSSet setWithArray:@[ + @"Alerts", + @"Attributes", + @"Scrolling", + @"Deadlock app", + @"Touch", + ]]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, expectedLabels.count); + NSArray *labels = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.label"]; + XCTAssertEqualObjects([NSSet setWithArray:labels], expectedLabels); + + NSArray *types = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.elementType"]; + XCTAssertEqual(types.count, 1, @"matchingSnapshots should contain only one type"); + XCTAssertEqualObjects(types.lastObject, @(XCUIElementTypeButton), @"matchingSnapshots should contain only one type"); +} + +- (void)testSingleDescendantWithClassName +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); +} + +- (void)testDescendantsWithIdentifier +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:NO]; + int snapshotsCount = 2; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); + XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); + NSArray *selfElementsById = [matchingSnapshots.lastObject fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(selfElementsById.count, 1); +} + +- (void)testSingleDescendantWithIdentifier +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testStableInstance +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 1); + for (XCUIElement *el in @[ + matchingSnapshots.lastObject, + [matchingSnapshots.lastObject fb_stableInstanceWithUid:[matchingSnapshots.lastObject fb_uid]] + ]) { + XCTAssertEqual(el.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(el.label, @"Alerts"); + } +} + +- (void)testSingleDescendantWithMissingIdentifier +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"blabla" shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 0); +} + +- (void)testDescendantsWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts']" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testSelfWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeApplication" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); +} + +- (void)testSingleDescendantWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@hittable='true']" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCUIElement *matchingSnapshot = [matchingSnapshots firstObject]; + XCTAssertNotNil(matchingSnapshot); + XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshot.label, @"Alerts"); +} + +- (void)testSingleDescendantWithXPathQueryNoMatches +{ + XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButtonnn" + shouldReturnAfterFirstMatch:YES] firstObject]; + XCTAssertNil(matchingSnapshot); +} + +- (void)testSingleLastDescendantWithXPathQuery +{ + XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"(//XCUIElementTypeButton)[last()]" + shouldReturnAfterFirstMatch:YES] firstObject]; + XCTAssertNotNil(matchingSnapshot); + XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); +} + +- (void)testDescendantsWithXPathQueryNoMatches +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts1']" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 0); +} + +- (void)testDescendantsWithComplexXPathQuery +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//*[@label='Scrolling']/preceding::*[boolean(string(@label))]" + shouldReturnAfterFirstMatch:NO]; + int snapshotsCount = 6; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); +} + +- (void)testDescendantsWithWrongXPathQuery +{ + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" + shouldReturnAfterFirstMatch:NO], + NSException, FBInvalidXPathException); +} + +- (void)testFirstDescendantWithWrongXPathQuery +{ + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" + shouldReturnAfterFirstMatch:YES], + NSException, FBInvalidXPathException); +} + +- (void)testVisibleDescendantWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@name='Alerts' and @enabled='true' and @visible='true']" shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); + XCTAssertTrue(matchingSnapshots.lastObject.isEnabled); + XCTAssertTrue(matchingSnapshots.lastObject.fb_isVisible); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testDescendantsWithPredicateString +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label = 'Alerts'"]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate + shouldReturnAfterFirstMatch:NO]; + int snapshotsCount = 2; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); + XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); + NSPredicate *selfPredicate = [NSPredicate predicateWithFormat:@"label == 'Alerts'"]; + NSArray *selfElementsByPredicate = [matchingSnapshots.lastObject fb_descendantsMatchingPredicate:selfPredicate + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(selfElementsByPredicate.count, 1); +} + +- (void)testSelfWithPredicateString +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == 'XCUIElementTypeApplication'"]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingPredicate:predicate + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); +} + +- (void)testSingleDescendantWithPredicateString +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type = 'XCUIElementTypeButton'"]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); +} + +- (void)testSingleDescendantWithPredicateStringByIndex +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == 'XCUIElementTypeButton' AND index == 2"]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); +} + +- (void)testDescendantsWithPropertyStrict +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" + value:@"Alert" + partialSearch:NO]; + XCTAssertEqual(matchingSnapshots.count, 0); + matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; + int snapshotsCount = 2; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); + XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testGlobalWithPropertyStrict +{ + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" + value:@"Alert" + partialSearch:NO]; + XCTAssertEqual(matchingSnapshots.count, 0); + matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; + int snapshotsCount = 2; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); + XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testDescendantsWithPropertyPartial +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" + value:@"Alerts" + partialSearch:NO]; + int snapshotsCount = 2; + XCTAssertEqual(matchingSnapshots.count, snapshotsCount); + XCTAssertEqual(matchingSnapshots.firstObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); +} + +- (void)testDescendantsWithClassChain +{ + NSArray *matchingSnapshots; + NSString *queryString =@"XCUIElementTypeWindow/XCUIElementTypeOther/**/XCUIElementTypeButton"; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton + for (XCUIElement *matchingSnapshot in matchingSnapshots) { + XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); + } +} + +- (void)testDescendantsWithClassChainWithIndex +{ + NSArray *matchingSnapshots; + // iPhone + NSString *queryString = @"XCUIElementTypeWindow/*/*/*/*[2]/*/*/XCUIElementTypeButton"; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + if (matchingSnapshots.count == 0) { + // iPad + queryString = @"XCUIElementTypeWindow/*/*/*/*/*[2]/*/*/XCUIElementTypeButton"; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + } + XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton + for (XCUIElement *matchingSnapshot in matchingSnapshots) { + XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); + } +} + +- (void)testDescendantsWithClassChainAndPredicates +{ + NSArray *matchingSnapshots; + NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 2); + XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); + XCTAssertEqualObjects([matchingSnapshots lastObject].label, @"Attributes"); +} + +- (void)testDescendantsWithIndirectClassChainAndPredicates +{ + NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; + NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + NSArray *deepQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(simpleQueryMatches.count, deepQueryMatches.count); + XCTAssertEqualObjects([simpleQueryMatches firstObject].label, [deepQueryMatches firstObject].label); + XCTAssertEqualObjects([simpleQueryMatches lastObject].label, [deepQueryMatches lastObject].label); +} + +- (void)testClassChainWithDescendantPredicate +{ + NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[1]" + shouldReturnAfterFirstMatch:NO]; + NSArray *predicateQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[$type == 'XCUIElementTypeButton' AND label BEGINSWITH 'A'$]" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(simpleQueryMatches.count, predicateQueryMatches.count); + XCTAssertEqual([simpleQueryMatches firstObject].elementType, [predicateQueryMatches firstObject].elementType); + XCTAssertEqual([simpleQueryMatches lastObject].elementType, [predicateQueryMatches lastObject].elementType); +} + +- (void)testSingleDescendantWithComplexIndirectClassChain +{ + NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeButton[2]" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(queryMatches.count, 1); + XCTAssertEqual(queryMatches.lastObject.elementType, XCUIElementTypeButton); + XCTAssertEqualObjects(queryMatches.lastObject.label, @"Deadlock app"); +} + +- (void)testSingleDescendantWithComplexIndirectClassChainAndZeroMatches +{ + NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeWindow" + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(queryMatches.count, 0); +} + +- (void)testDescendantsWithClassChainAndPredicatesAndIndexes +{ + NSArray *matchingSnapshots; + NSString *queryString = @"XCUIElementTypeWindow[`name != 'bla'`]/**/XCUIElementTypeButton[`label BEGINSWITH \"A\"`][1]"; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); +} + +- (void)testSingleDescendantWithClassChain +{ + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:YES]; + + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); + XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Alerts"]); +} + +- (void)testSingleDescendantWithClassChainAndNegativeIndex +{ + NSArray *matchingSnapshots; + matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-1]" + shouldReturnAfterFirstMatch:YES]; + + XCTAssertEqual(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); + XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Touch"]); + + matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-10]" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 0); +} + +- (void)testInvalidQueryWithClassChain +{ + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingClassChain:@"NoXCUIElementTypePrefix" + shouldReturnAfterFirstMatch:YES], + NSException, FBClassChainQueryParseException); +} + +- (void)testHandleInvalidQueryWithClassChainAsNoElementWithoutError +{ + NSArray *matchingSnapshots = [self.testedView + fb_descendantsMatchingClassChain:@"XCUIElementTypeBlabla" + shouldReturnAfterFirstMatch:YES]; + XCTAssertEqual(matchingSnapshots.count, 0); +} + +- (void)testClassChainWithInvalidPredicate +{ + XCTAssertThrowsSpecificNamed([self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow[`bla != 'bla'`]" + shouldReturnAfterFirstMatch:NO], + NSException, FBUnknownAttributeException);; +} + +@end + +@interface XCUIElementFBFindTests_AttributesPage : FBIntegrationTestCase +@end +@implementation XCUIElementFBFindTests_AttributesPage + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToAttributesPage]; + }); +} + +- (void)testNestedQueryWithClassChain +{ + NSString *queryString = @"XCUIElementTypePicker"; + FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Button"].fb_isVisible); + XCUIElement *datePicker = [self.testedApplication + descendantsMatchingType:XCUIElementTypeDatePicker].allElementsBoundByIndex.firstObject; + NSArray *matches = [datePicker fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + XCTAssertEqual(matches.count, 1); + + XCUIElementType expectedType = XCUIElementTypePicker; + XCTAssertEqual([matches firstObject].elementType, expectedType); +} + +@end + +@interface XCUIElementFBFindTests_ScrollPage : FBIntegrationTestCase +@end +@implementation XCUIElementFBFindTests_ScrollPage + +- (void)setUp +{ + [super setUp]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self launchApplication]; + [self goToScrollPageWithCells:YES]; + }); +} + +- (void)testInvisibleDescendantWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@visible='false']" + shouldReturnAfterFirstMatch:NO]; + XCTAssertGreaterThan(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeStaticText); + XCTAssertFalse(matchingSnapshots.lastObject.fb_isVisible); +} + +- (void)testNonHittableDescendantWithXPathQuery +{ + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@hittable='false']" + shouldReturnAfterFirstMatch:NO]; + XCTAssertGreaterThan(matchingSnapshots.count, 1); + XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeStaticText); + XCTAssertFalse(matchingSnapshots.lastObject.fb_isVisible); +} + +@end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m new file mode 100644 index 0000000..531a962 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "XCTest/XCUIElementTypes.h" + +#import "FBIntegrationTestCase.h" +#import "FBTestMacros.h" +#import "FBElement.h" +#import "FBElementUtils.h" +#import "FBXCElementSnapshot.h" +#import "XCUIElement+FBUtilities.h" + +@interface XCUIElementHelperIntegrationTests : FBIntegrationTestCase +@end + +@implementation XCUIElementHelperIntegrationTests + +- (void)setUp +{ + [super setUp]; + [self launchApplication]; + [self goToAlertsPage]; +} + +- (void)testDescendantsFiltering +{ + NSArray *buttons = self.testedApplication.buttons.allElementsBoundByIndex; + XCTAssertTrue(buttons.count > 0); + NSArray *windows = self.testedApplication.windows.allElementsBoundByIndex; + XCTAssertTrue(windows.count > 0); + + NSMutableArray *allElements = [NSMutableArray array]; + [allElements addObjectsFromArray:buttons]; + [allElements addObjectsFromArray:windows]; + + NSMutableArray> *buttonSnapshots = [NSMutableArray array]; + [buttonSnapshots addObject:[buttons.firstObject fb_customSnapshot]]; + + NSArray *result = [self.testedApplication fb_filterDescendantsWithSnapshots:buttonSnapshots + onlyChildren:NO]; + XCTAssertEqual(1, result.count); + XCTAssertEqual([result.firstObject elementType], XCUIElementTypeButton); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h new file mode 100644 index 0000000..140547f --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface XCElementSnapshotDouble : NSObject +@property (readwrite, nullable) id value; +@property (readwrite, nullable, copy) NSString *label; +@property (nonatomic, assign) UIAccessibilityTraits traits; +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m new file mode 100644 index 0000000..f8592ac --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCElementSnapshotDouble.h" + +#import "FBXCAccessibilityElement.h" +#import "FBXCElementSnapshot.h" +#import "XCUIHitPointResult.h" + +@implementation XCElementSnapshotDouble + +- (id)init +{ + self = [super init]; + self->_value = @"magicValue"; + self->_label = @"testLabel"; + return self; +} + +- (NSString *)identifier +{ + return @"testName"; +} + +- (CGRect)frame +{ + return CGRectZero; +} + +- (NSString *)title +{ + return @"testTitle"; +} + +- (XCUIElementType)elementType +{ + return XCUIElementTypeOther; +} + +- (BOOL)isEnabled +{ + return YES; +} + +- (XCUIUserInterfaceSizeClass)horizontalSizeClass +{ + return XCUIUserInterfaceSizeClassUnspecified; +} + +- (XCUIUserInterfaceSizeClass)verticalSizeClass +{ + return XCUIUserInterfaceSizeClassUnspecified; +} + +- (NSString *)placeholderValue +{ + return @"testPlaceholderValue"; +} + +- (BOOL)isSelected +{ + return YES; +} + +- (BOOL)hasFocus +{ + return YES; +} + +- (NSDictionary *)additionalAttributes +{ + return @{}; +} + +- (id)accessibilityElement +{ + return nil; +} + +- (id)parent +{ + return nil; +} + +- (XCUIHitPointResult *)hitPoint:(NSError **)error +{ + return [[XCUIHitPointResult alloc] initWithHitPoint:CGPointZero hittable:YES]; +} + +- (NSArray *)children +{ + return @[]; +} + +- (NSArray *)_allDescendants +{ + return @[]; +} + +- (CGRect)visibleFrame +{ + return CGRectZero; +} + +- (UIAccessibilityTraits)traits +{ + return UIAccessibilityTraitButton; +} +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h new file mode 100644 index 0000000..f0d7b59 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface XCUIApplicationDouble : NSObject +@property (nonatomic, assign, readonly) BOOL didTerminate; +@property (nonatomic, strong) NSString* bundleID; +@property (nonatomic) BOOL fb_shouldWaitForQuiescence; + +- (BOOL)running; +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m new file mode 100644 index 0000000..2cff790 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIApplicationDouble.m @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIApplicationDouble.h" + +@interface XCUIApplicationDouble () +@property (nonatomic, assign, readwrite) BOOL didTerminate; +@end + +@implementation XCUIApplicationDouble + +- (instancetype)init +{ + self = [super init]; + if (self) { + _bundleID = @"some.bundle.identifier"; + } + return self; +} + +- (void)terminate +{ + self.didTerminate = YES; +} + +- (NSUInteger)processID +{ + return 0; +} + +- (NSString *)bundleID +{ + return @"com.facebook.awesome"; +} + +- (void)fb_nativeResolve +{ + +} + +- (id)query +{ + return nil; +} + +- (BOOL)fb_shouldWaitForQuiescence +{ + return NO; +} + +-(void)setFb_shouldWaitForQuiescence:(BOOL)value +{ + +} + +- (BOOL)running +{ + return NO; +} + +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h new file mode 100644 index 0000000..90d523b --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +@class XCUIApplication; + +@interface XCUIElementDouble : NSObject +@property (nonatomic, strong, nonnull) XCUIApplication *application; +@property (nonatomic, readwrite, assign) CGRect frame; +@property (nonatomic, readwrite, nullable) id lastSnapshot; +@property (nonatomic, assign) BOOL fb_isObstructedByAlert; +@property (nonatomic, readonly, nonnull) NSString *fb_cacheId; +@property (nonatomic, readwrite, copy, nonnull) NSDictionary *wdRect; +@property (nonatomic, readwrite, assign) CGRect wdFrame; +@property (nonatomic, readwrite, copy, nonnull) NSString *wdUID; +@property (nonatomic, copy, readwrite, nullable) NSString *wdName; +@property (nonatomic, copy, readwrite, nullable) NSString *wdLabel; +@property (nonatomic, copy, readwrite, nonnull) NSString *wdType; +@property (nonatomic, strong, readwrite, nullable) NSString *wdValue; +@property (nonatomic, readwrite, getter=isWDEnabled) BOOL wdEnabled; +@property (nonatomic, readwrite, getter=isWDSelected) BOOL wdSelected; +@property (nonatomic, readwrite, assign) CGRect wdNativeFrame; +@property (nonatomic, readwrite) NSUInteger wdIndex; +@property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; +@property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; +@property (nonatomic, readwrite, getter = isWDFocused) BOOL wdFocused; +@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; +@property (nonatomic, copy, readwrite, nullable) NSString *wdPlaceholderValue; +@property (copy, nonnull) NSArray *children; +@property (nonatomic, readwrite, assign) XCUIElementType elementType; +@property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; +@property (nonatomic, copy, readwrite, nullable) NSString *wdTraits; + +- (void)resolve; +- (id _Nonnull)fb_standardSnapshot; +- (id _Nonnull)fb_customSnapshot; +- (nullable id)query; + +// Checks +@property (nonatomic, assign, readonly) BOOL didResolve; + +@end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m new file mode 100644 index 0000000..f581346 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElementDouble.h" + +@interface XCUIElementDouble () +@property (nonatomic, assign, readwrite) BOOL didResolve; +@end + +@implementation XCUIElementDouble + +- (id)init +{ + self = [super init]; + if (self) { + self.wdFrame = CGRectZero; + self.wdNativeFrame = CGRectZero; + self.wdName = @"testName"; + self.wdLabel = @"testLabel"; + self.wdValue = @"magicValue"; + self.wdPlaceholderValue = @"testPlaceholderValue"; + self.wdTraits = @"testTraits"; + self.wdVisible = YES; + self.wdAccessible = YES; + self.wdEnabled = YES; + self.wdSelected = YES; + self.wdFocused = YES; + self.wdHittable = YES; + self.wdIndex = 0; +#if TARGET_OS_TV + self.wdFocused = YES; +#endif + self.children = @[]; + self.wdRect = @{@"x": @0, + @"y": @0, + @"width": @0, + @"height": @0, + }; + self.wdAccessibilityContainer = NO; + self.elementType = XCUIElementTypeOther; + self.wdType = @"XCUIElementTypeOther"; + self.wdUID = @"0"; + self.lastSnapshot = nil; + } + return self; +} + +- (id)fb_valueForWDAttributeName:(NSString *)name +{ + return @"test"; +} + +- (id)query +{ + return nil; +} + +- (void)resolve +{ + self.didResolve = YES; +} + +- (void)fb_nativeResolve +{ + self.didResolve = YES; +} + +- (id _Nonnull)fb_standardSnapshot; +{ + return [self lastSnapshot]; +} + +- (id _Nonnull)fb_customSnapshot; +{ + return [self lastSnapshot]; +} + +- (NSString *)fb_cacheId +{ + return self.wdUID; +} + +- (id)lastSnapshot +{ + return self; +} + +- (id)fb_uid +{ + return self.wdUID; +} + +- (NSString *)wdTraits +{ + return self.wdTraits; +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBClassChainTests.m b/WebDriverAgentTests/UnitTests/FBClassChainTests.m new file mode 100644 index 0000000..0409af6 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBClassChainTests.m @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "XCUIElementDouble.h" +#import "FBClassChainQueryParser.h" + +@interface FBClassChainTests : XCTestCase +@end + +@implementation FBClassChainTests + +- (void)testValidChain +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); + XCTAssertNil(firstElement.position); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertNil(secondElement.position); + XCTAssertFalse(secondElement.isDescendant); +} + +- (void)testValidChainWithStar +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton[3]/*[4]/*[5]/XCUIElementTypeAlert" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 5); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); + XCTAssertNil(firstElement.position); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertEqual(secondElement.position.integerValue, 3); + XCTAssertFalse(secondElement.isDescendant); + + FBClassChainItem *thirdElement = [result.elements objectAtIndex:2]; + XCTAssertEqual(thirdElement.type, XCUIElementTypeAny); + XCTAssertEqual(thirdElement.position.integerValue, 4); + XCTAssertFalse(thirdElement.isDescendant); + + FBClassChainItem *fourthElement = [result.elements objectAtIndex:3]; + XCTAssertEqual(fourthElement.type, XCUIElementTypeAny); + XCTAssertEqual(fourthElement.position.integerValue, 5); + XCTAssertFalse(fourthElement.isDescendant); + + FBClassChainItem *fifthsElement = [result.elements objectAtIndex:4]; + XCTAssertEqual(fifthsElement.type, XCUIElementTypeAlert); + XCTAssertNil(fifthsElement.position); + XCTAssertFalse(fifthsElement.isDescendant); +} + +- (void)testValidSingleStarChain +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"*" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 1); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeAny); + XCTAssertNil(firstElement.position); + XCTAssertFalse(firstElement.isDescendant); +} + +- (void)testValidSingleStarIndirectChain +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/*/*/XCUIElementTypeButton" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 3); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeAny); + XCTAssertNil(firstElement.position); + XCTAssertTrue(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeAny); + XCTAssertNil(secondElement.position); + XCTAssertFalse(secondElement.isDescendant); + + FBClassChainItem *thirdElement = [result.elements objectAtIndex:2]; + XCTAssertEqual(thirdElement.type, XCUIElementTypeButton); + XCTAssertNil(thirdElement.position); + XCTAssertFalse(thirdElement.isDescendant); +} + +- (void)testValidDoubleIndirectChainAndStar +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeButton/**/*" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeButton); + XCTAssertNil(firstElement.position); + XCTAssertTrue(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeAny); + XCTAssertNil(secondElement.position); + XCTAssertTrue(secondElement.isDescendant); +} + +- (void)testValidDoubleIndirectChainAndClassName +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeButton/**/XCUIElementTypeImage" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeButton); + XCTAssertNil(firstElement.position); + XCTAssertTrue(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeImage); + XCTAssertNil(secondElement.position); + XCTAssertTrue(secondElement.isDescendant); +} + +- (void)testValidChainWithNegativeIndex +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow/XCUIElementTypeButton[-1]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); + XCTAssertNil(firstElement.position); + XCTAssertEqual(firstElement.predicates.count, 0); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertEqual(secondElement.position.integerValue, -1); + XCTAssertEqual(secondElement.predicates.count, 0); + XCTAssertFalse(secondElement.isDescendant); +} + +- (void)testValidChainWithSinglePredicate +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow[`name == 'blabla'`]/XCUIElementTypeButton" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); + XCTAssertNil(firstElement.position); + XCTAssertEqual(firstElement.predicates.count, 1); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertNil(secondElement.position); + XCTAssertEqual(secondElement.predicates.count, 0); + XCTAssertFalse(secondElement.isDescendant); +} + +- (void)testValidChainWithMultiplePredicates +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"XCUIElementTypeWindow[`name == 'blabla'`]/XCUIElementTypeButton[`value == 'blabla'`]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeWindow); + XCTAssertNil(firstElement.position); + XCTAssertEqual(firstElement.predicates.count, 1); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertNil(secondElement.position); + XCTAssertEqual(secondElement.predicates.count, 1); + XCTAssertFalse(secondElement.isDescendant); +} + +- (void)testValidChainWithIndirectSearchAndPredicates +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[`name == 'blabla'`][10]/**/XCUIElementTypeButton[`value == 'blabla'`]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeTable); + XCTAssertEqual(firstElement.position.integerValue, 10); + XCTAssertEqual(firstElement.predicates.count, 1); + XCTAssertTrue(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertNil(secondElement.position); + XCTAssertEqual(secondElement.predicates.count, 1); + XCTAssertTrue(secondElement.isDescendant); +} + +- (void)testValidChainWithMultiplePredicatesAndPositions +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"*[`name == \"к``ири````'лиця\"`][3]/XCUIElementTypeButton[`value == \"blabla\"`][-1]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 2); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeAny); + XCTAssertEqual(firstElement.position.integerValue, 3); + XCTAssertEqual(firstElement.predicates.count, 1); + XCTAssertFalse(firstElement.isDescendant); + + FBClassChainItem *secondElement = [result.elements objectAtIndex:1]; + XCTAssertEqual(secondElement.type, XCUIElementTypeButton); + XCTAssertEqual(secondElement.position.integerValue, -1); + XCTAssertEqual(secondElement.predicates.count, 1); + XCTAssertFalse(secondElement.isDescendant); +} + +- (void)testValidChainWithDescendantPredicate +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[$type == 'XCUIElementTypeImage' AND name == 'olala'$][`name == 'blabla'`][10]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 1); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeTable); + XCTAssertEqual(firstElement.position.integerValue, 10); + XCTAssertEqual(firstElement.predicates.count, 2); + XCTAssertTrue(firstElement.isDescendant); +} + +- (void)testValidChainWithMultipleDescendantPredicates +{ + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:@"**/XCUIElementTypeTable[$type == 'XCUIElementTypeImage' AND name == 'olala'$][`value == 'peace'`][$value == 'yolo'$][`name == 'blabla'`][10]" error:&error]; + XCTAssertNotNil(result); + XCTAssertEqual(result.elements.count, 1); + + FBClassChainItem *firstElement = [result.elements firstObject]; + XCTAssertEqual(firstElement.type, XCUIElementTypeTable); + XCTAssertEqual(firstElement.position.integerValue, 10); + XCTAssertEqual(firstElement.predicates.count, 4); + XCTAssertTrue(firstElement.isDescendant); +} + +- (void)testInvalidChains +{ + NSArray *invalidQueries = @[ + @"/XCUIElementTypeWindow" + ,@"XCUIElementTypeWindow/" + ,@"XCUIElementTypeWindow//*" + ,@"XCUIElementTypeWindow*/*" + ,@"**" + ,@"***" + ,@"**/*/**" + ,@"/**" + ,@"XCUIElementTypeWindow/**" + ,@"**[1]/XCUIElementTypeWindow" + ,@"**[`name == '1'`]/XCUIElementTypeWindow" + ,@"XCUIElementTypeWindow[0]" + ,@"XCUIElementTypeWindow[1][1]" + ,@"blabla" + ,@"XCUIElementTypeWindow/blabla" + ,@" XCUIElementTypeWindow" + ,@"XCUIElementTypeWindow[ 2 ]" + ,@"XCUIElementTypeWindow[[2]" + ,@"XCUIElementTypeWindow[2]]" + ,@"XCUIElementType[Window[2]]" + ,@"XCUIElementTypeWindow[visible = 1]" + ,@"XCUIElementTypeWindow[1][`visible = 1`]" + ,@"XCUIElementTypeWindow[1] [`visible = 1`]" + ,@"XCUIElementTypeWindow[ `visible = 1`]" + ,@"XCUIElementTypeWindow[`visible = 1][`name = \"bla\"`]" + ,@"XCUIElementTypeWindow[`visible = 1]" + ,@"XCUIElementTypeWindow[$visible = 1]" + ,@"XCUIElementTypeWindow[``]" + ,@"XCUIElementTypeWindow[$$]" + ,@"XCUIElementTypeWindow[`name = \"bla```bla\"`]" + ,@"XCUIElementTypeWindow[$name = \"bla$$$bla\"$]" + ]; + for (NSString *invalidQuery in invalidQueries) { + NSError *error; + FBClassChain *result = [FBClassChainQueryParser parseQuery:invalidQuery error:&error]; + XCTAssertNil(result); + XCTAssertNotNil(error); + } +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBConfigurationTests.m b/WebDriverAgentTests/UnitTests/FBConfigurationTests.m new file mode 100644 index 0000000..05e53d6 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBConfigurationTests.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBConfiguration.h" + +@interface FBConfigurationTests : XCTestCase + +@end + +@implementation FBConfigurationTests + +- (void)setUp +{ + [super setUp]; + unsetenv("USE_PORT"); + unsetenv("VERBOSE_LOGGING"); +} + +- (void)testBindingPortDefault +{ + XCTAssertTrue(NSEqualRanges([FBConfiguration bindingPortRange], NSMakeRange(8100, 100))); +} + +- (void)testBindingPortEnvironmentOverwrite +{ + setenv("USE_PORT", "1000", 1); + XCTAssertTrue(NSEqualRanges([FBConfiguration bindingPortRange], NSMakeRange(1000, 1))); +} + +- (void)testVerboseLoggingDefault +{ + XCTAssertFalse([FBConfiguration verboseLoggingEnabled]); +} + +- (void)testVerboseLoggingEnvironmentOverwrite +{ + setenv("VERBOSE_LOGGING", "YES", 1); + XCTAssertTrue([FBConfiguration verboseLoggingEnabled]); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBElementCacheTests.m b/WebDriverAgentTests/UnitTests/FBElementCacheTests.m new file mode 100644 index 0000000..11e0c6f --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBElementCacheTests.m @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBElementCache.h" +#import "XCUIElementDouble.h" +#import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUtilities.h" + +@interface FBElementCacheTests : XCTestCase +@property (nonatomic, strong) FBElementCache *cache; +@end + +@implementation FBElementCacheTests + +- (void)setUp +{ + [super setUp]; + self.cache = [FBElementCache new]; +} + +- (void)testStoringElement +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + el1.wdUID = @"1"; + XCUIElementDouble *el2 = XCUIElementDouble.new; + el2.wdUID = @"2"; + NSString *firstUUID = [self.cache storeElement:(XCUIElement *)el1]; + NSString *secondUUID = [self.cache storeElement:(XCUIElement *)el2]; + XCTAssertEqualObjects(firstUUID, el1.wdUID); + XCTAssertEqualObjects(secondUUID, el2.wdUID); +} + +- (void)testFetchingElement +{ + XCUIElement *element = (XCUIElement *)XCUIElementDouble.new; + NSString *uuid = [self.cache storeElement:element]; + XCTAssertNotNil(uuid, @"Stored index should be higher than 0"); + XCUIElement *cachedElement = [self.cache elementForUUID:uuid]; + XCTAssertEqual(element, cachedElement); +} + +- (void)testFetchingBadIndex +{ + XCTAssertThrows([self.cache elementForUUID:@"random"]); +} + +- (void)testLinearCacheExpulsion +{ + const int ELEMENT_COUNT = 1050; + + NSMutableArray *elements = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; + NSMutableArray *elementIds = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; + for(int i = 0; i < ELEMENT_COUNT; i++) { + XCUIElementDouble *el = XCUIElementDouble.new; + el.wdUID = [NSString stringWithFormat:@"%@", @(i)]; + [elements addObject:(XCUIElement *)el]; + } + + // The capacity of the cache is limited to 1024 elements. Add 1050 + // elements and make sure: + // - The first 26 elements are no longer present in the cache + // - The remaining 1024 elements are present in the cache + for(int i = 0; i < ELEMENT_COUNT; i++) { + [elementIds addObject:[self.cache storeElement:elements[i]]]; + } + + for(int i = 0; i < ELEMENT_COUNT - ELEMENT_CACHE_SIZE; i++) { + XCTAssertThrows([self.cache elementForUUID:elementIds[i]]); + } + for(int i = ELEMENT_COUNT - ELEMENT_CACHE_SIZE; i < ELEMENT_COUNT; i++) { + XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); + } +} + +- (void)testMRUCacheExpulsion +{ + const int ELEMENT_COUNT = 1050; + const int ACCESSED_ELEMENT_COUNT = 24; + + NSMutableArray *elements = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; + NSMutableArray *elementIds = [NSMutableArray arrayWithCapacity:ELEMENT_COUNT]; + for(int i = 0; i < ELEMENT_COUNT; i++) { + XCUIElementDouble *el = XCUIElementDouble.new; + el.wdUID = [NSString stringWithFormat:@"%@", @(i)]; + [elements addObject:(XCUIElement *)el]; + } + + // The capacity of the cache is limited to 1024 elements. Add 1050 + // elements, but with a twist: access the first 24 elements before + // adding the last 50 elements. Then, make sure: + // - The first 24 elements are present in the cache + // - The next 26 elements are not present in the cache + // - The remaining 1000 elements are present in the cache + for(int i = 0; i < ELEMENT_CACHE_SIZE; i++) { + [elementIds addObject:[self.cache storeElement:elements[i]]]; + } + + for(int i = 0; i < ACCESSED_ELEMENT_COUNT; i++) { + [self.cache elementForUUID:elementIds[i]]; + } + + for(int i = ELEMENT_CACHE_SIZE; i < ELEMENT_COUNT; i++) { + [elementIds addObject:[self.cache storeElement:elements[i]]]; + } + + for(int i = 0; i < ACCESSED_ELEMENT_COUNT; i++) { + XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); + } + for(int i = ACCESSED_ELEMENT_COUNT; i < ELEMENT_COUNT - ELEMENT_CACHE_SIZE + ACCESSED_ELEMENT_COUNT; i++) { + XCTAssertThrows([self.cache elementForUUID:elementIds[i]]); + } + for(int i = ELEMENT_COUNT - ELEMENT_CACHE_SIZE + ACCESSED_ELEMENT_COUNT; i < ELEMENT_COUNT; i++) { + XCTAssertEqual(elements[i], [self.cache elementForUUID:elementIds[i]]); + } +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m b/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m new file mode 100644 index 0000000..ad1aa98 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBElementTypeTransformerTests.m @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBElementTypeTransformer.h" + +@interface FBElementTypeTransformerTests : XCTestCase +@end + +@implementation FBElementTypeTransformerTests + +- (void)testStringWithElementType +{ + XCTAssertEqualObjects(@"XCUIElementTypeAny", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeAny]); + XCTAssertEqualObjects(@"XCUIElementTypeIcon", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeIcon]); + XCTAssertEqualObjects(@"XCUIElementTypeTab", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeTab]); + XCTAssertEqualObjects(@"XCUIElementTypeOther", [FBElementTypeTransformer stringWithElementType:XCUIElementTypeOther]); +} + +- (void)testShortStringWithElementType +{ + XCTAssertEqualObjects(@"Any", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeAny]); + XCTAssertEqualObjects(@"Icon", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeIcon]); + XCTAssertEqualObjects(@"Tab", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeTab]); + XCTAssertEqualObjects(@"Other", [FBElementTypeTransformer shortStringWithElementType:XCUIElementTypeOther]); +} + +- (void)testElementTypeWithElementTypeName +{ + XCTAssertEqual(XCUIElementTypeAny, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeAny"]); + XCTAssertEqual(XCUIElementTypeIcon, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeIcon"]); + XCTAssertEqual(XCUIElementTypeTab, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeTab"]); + XCTAssertEqual(XCUIElementTypeOther, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeOther"]); + XCTAssertThrows([FBElementTypeTransformer elementTypeWithTypeName:@"Whatever"]); + XCTAssertThrows([FBElementTypeTransformer elementTypeWithTypeName:nil]); + XCTAssertEqual(XCUIElementTypeOther, [FBElementTypeTransformer elementTypeWithTypeName:@"XCUIElementTypeNewType"]); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m b/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m new file mode 100644 index 0000000..5dad1c7 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +#import + +#import "FBElement.h" +#import "XCUIElementDouble.h" +#import "FBElementUtils.h" + +@interface FBElementUtilitiesTests : XCTestCase +@end + +@implementation FBElementUtilitiesTests + +- (void)testTypesFiltering { + NSMutableArray *elements = [NSMutableArray new]; + XCUIElementDouble *el1 = [XCUIElementDouble new]; + [elements addObject:el1]; + XCUIElementDouble *el2 = [XCUIElementDouble new]; + el2.elementType = XCUIElementTypeAlert; + el2.wdType = @"XCUIElementTypeAlert"; + [elements addObject:el2]; + XCUIElementDouble *el3 = [XCUIElementDouble new]; + [elements addObject:el3]; + + NSSet *result = [FBElementUtils uniqueElementTypesWithElements:elements]; + XCTAssertEqual([result count], 2); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m b/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m new file mode 100644 index 0000000..cc28116 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBErrorBuilderTests.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBErrorBuilder.h" + +@interface FBErrorBuilderTests : XCTestCase +@end + +@implementation FBErrorBuilderTests + +- (void)testErrorWithDescription +{ + NSString *expectedDescription = @"Magic description"; + NSError *error = + [[[FBErrorBuilder builder] + withDescription:expectedDescription] + build]; + XCTAssertNotNil(error); + XCTAssertEqualObjects([error localizedDescription], expectedDescription); +} + +- (void)testErrorWithDescriptionFormat +{ + NSError *error = + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Magic %@", @"bob"] + build]; + XCTAssertEqualObjects([error localizedDescription], @"Magic bob"); +} + +- (void)testInnerError +{ + NSError *innerError = [NSError errorWithDomain:@"Domain" code:1 userInfo:@{}]; + NSError *error = + [[[FBErrorBuilder builder] + withInnerError:innerError] + build]; + XCTAssertEqual(error.userInfo[NSUnderlyingErrorKey], innerError); +} + +- (void)testBuildWithError +{ + NSString *expectedDescription = @"Magic description"; + NSError *error; + BOOL result = + [[[FBErrorBuilder builder] + withDescription:expectedDescription] + buildError:&error]; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.localizedDescription, expectedDescription); + XCTAssertFalse(result); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m b/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m new file mode 100644 index 0000000..64845dd --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBExceptionHandlerTests.m @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBAlert.h" +#import "FBExceptionHandler.h" +#import "FBExceptions.h" + + +@interface RouteResponseDouble : NSObject +- (void)setHeader:(NSString *)field value:(NSString *)value; +- (void)setStatusCode:(NSUInteger)code; +- (void)respondWithData:(NSData *)data; +@end + +@implementation RouteResponseDouble +- (void)setHeader:(NSString *)field value:(NSString *)value {} +- (void)setStatusCode:(NSUInteger)code {} +- (void)respondWithData:(NSData *)data {} +@end + + +@interface FBExceptionHandlerTests : XCTestCase +@property (nonatomic) FBExceptionHandler *exceptionHandler; +@end + +@implementation FBExceptionHandlerTests + +- (void)setUp +{ + self.exceptionHandler = [FBExceptionHandler new]; +} + +- (void)testMatchingErrorHandling +{ + NSException *exception = [NSException exceptionWithName:FBElementNotVisibleException + reason:@"reason" + userInfo:@{}]; + [self.exceptionHandler handleException:exception + forResponse:(RouteResponse *)[RouteResponseDouble new]]; +} + +- (void)testNonMatchingErrorHandling +{ + NSException *exception = [NSException exceptionWithName:@"something" + reason:@"reason" + userInfo:@{}]; + [self.exceptionHandler handleException:exception + forResponse:(RouteResponse *)[RouteResponseDouble new]]; +} + + +@end diff --git a/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m b/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m new file mode 100644 index 0000000..e8c6528 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "LRUCache.h" + +@interface FBLRUCacheTests : XCTestCase +@end + +@implementation FBLRUCacheTests + +- (void)assertArray:(NSArray *)array1 equalsTo:(NSArray *)array2 +{ + XCTAssertEqualObjects(array1, array2); +} + +- (void)testRecentlyInsertedObjectReplacesTheOldestOne +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:1]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache setObject:@"foo3" forKey:@"bar3"]; + XCTAssertEqualObjects(@[@"foo3"], cache.allObjects); +} + +- (void)testRecentObjectReplacementAndBump +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [self assertArray:@[@"foo2", @"foo"] equalsTo:cache.allObjects]; + XCTAssertNotNil([cache objectForKey:@"bar"]); + [self assertArray:@[@"foo", @"foo2"] equalsTo:cache.allObjects]; + [cache setObject:@"foo3" forKey:@"bar3"]; + [self assertArray:@[@"foo3", @"foo"] equalsTo:cache.allObjects]; + [cache setObject:@"foo0" forKey:@"bar"]; + [self assertArray:@[@"foo0", @"foo3"] equalsTo:cache.allObjects]; + [cache setObject:@"foo4" forKey:@"bar4"]; + [self assertArray:@[@"foo4", @"foo0"] equalsTo:cache.allObjects]; +} + +- (void)testBumpFromHead +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache setObject:@"foo3" forKey:@"bar3"]; + XCTAssertNotNil([cache objectForKey:@"bar3"]); + [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; + [cache setObject:@"foo4" forKey:@"bar4"]; + [cache setObject:@"foo5" forKey:@"bar5"]; + [self assertArray:@[@"foo5", @"foo4", @"foo3"] equalsTo:cache.allObjects]; +} + +- (void)testBumpFromMiddle +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache setObject:@"foo3" forKey:@"bar3"]; + XCTAssertNotNil([cache objectForKey:@"bar2"]); + [self assertArray:@[@"foo2", @"foo3", @"foo"] equalsTo:cache.allObjects]; + [cache setObject:@"foo4" forKey:@"bar4"]; + [cache setObject:@"foo5" forKey:@"bar5"]; + [self assertArray:@[@"foo5", @"foo4", @"foo2"] equalsTo:cache.allObjects]; +} + +- (void)testBumpFromTail +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache setObject:@"foo3" forKey:@"bar3"]; + XCTAssertNotNil([cache objectForKey:@"bar3"]); + [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; + [cache setObject:@"foo4" forKey:@"bar4"]; + [cache setObject:@"foo5" forKey:@"bar5"]; + [self assertArray:@[@"foo5", @"foo4", @"foo3"] equalsTo:cache.allObjects]; +} + +- (void)testInsertionLoop +{ + LRUCache *cache = [[LRUCache alloc] initWithCapacity:1]; + NSUInteger count = 100; + for (NSUInteger i = 0; i <= count; ++i) { + [cache setObject:@(i) forKey:@(i)]; + XCTAssertNotNil([cache objectForKey:@(i)]); + } + XCTAssertEqualObjects(@[@(count)], cache.allObjects); +} + +- (void)testRemoveExistingObjectForKey { + LRUCache *cache = [[LRUCache alloc] initWithCapacity:3]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache setObject:@"foo3" forKey:@"bar3"]; + [self assertArray:@[@"foo3", @"foo2", @"foo"] equalsTo:cache.allObjects]; + [cache removeObjectForKey:@"bar2"]; + XCTAssertNil([cache objectForKey:@"bar2"]); + [self assertArray:@[@"foo3", @"foo"] equalsTo:cache.allObjects]; +} + +- (void)testRemoveNonExistingObjectForKey { + LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache removeObjectForKey:@"nonExisting"]; + XCTAssertNotNil([cache objectForKey:@"bar"]); + [self assertArray:@[@"foo"] equalsTo:cache.allObjects]; +} + +- (void)testRemoveAndInsertFlow { + LRUCache *cache = [[LRUCache alloc] initWithCapacity:2]; + [cache setObject:@"foo" forKey:@"bar"]; + [cache setObject:@"foo2" forKey:@"bar2"]; + [cache removeObjectForKey:@"bar"]; + XCTAssertNil([cache objectForKey:@"bar"]); + [cache setObject:@"foo3" forKey:@"bar3"]; + [self assertArray:@[@"foo3", @"foo2"] equalsTo:cache.allObjects]; +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m b/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m new file mode 100644 index 0000000..8ca9b24 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBMathUtils.h" + +@interface FBMathUtilsTests : XCTestCase +@end + +@implementation FBMathUtilsTests + +- (void)testGetCenter +{ + XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(0, 0, 4, 4)), CGPointMake(2, 2))); + XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(1, 1, 4, 4)), CGPointMake(3, 3))); + XCTAssertTrue(CGPointEqualToPoint(FBRectGetCenter(CGRectMake(1, 3, 6, 14)), CGPointMake(4, 10))); +} + +- (void)testFuzzyEqualFloats +{ + XCTAssertTrue(FBFloatFuzzyEqualToFloat(0, 0, 0)); + XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.2)); + XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.6, 0.5, 0.2)); + XCTAssertTrue(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.10001)); +} + +- (void)testFuzzyNotEqualFloats +{ + XCTAssertFalse(FBFloatFuzzyEqualToFloat(0, 1, 0)); + XCTAssertFalse(FBFloatFuzzyEqualToFloat(1, 0, 0)); + XCTAssertFalse(FBFloatFuzzyEqualToFloat(0.5, 0.6, 0.05)); + XCTAssertFalse(FBFloatFuzzyEqualToFloat(0.6, 0.5, 0.05)); +} + +- (void)testFuzzyEqualPoints +{ + CGPoint referencePoint = CGPointMake(3, 3); + XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 3), 2)); + XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 4), 2)); + XCTAssertTrue(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(4, 3), 2)); +} + +- (void)testFuzzyNotEqualPoints +{ + CGPoint referencePoint = CGPointMake(3, 3); + XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(5, 5), 1)); + XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(3, 5), 1)); + XCTAssertFalse(FBPointFuzzyEqualToPoint(referencePoint, CGPointMake(5, 3), 1)); +} + +- (void)testFuzzyEqualSizes +{ + CGSize referenceSize = CGSizeMake(3, 3); + XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 3), 2)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 4), 2)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(4, 3), 2)); +} + +- (void)testFuzzyNotEqualSizes +{ + CGSize referenceSize = CGSizeMake(3, 3); + XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(5, 5), 1)); + XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(3, 5), 1)); + XCTAssertFalse(FBSizeFuzzyEqualToSize(referenceSize, CGSizeMake(5, 3), 1)); +} + +- (void)testFuzzyEqualRects +{ + CGRect referenceRect = CGRectMake(3, 3, 3, 3); + XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 3), 2)); + XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 4, 3, 3), 2)); + XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(4, 3, 3, 3), 2)); + XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 4), 2)); + XCTAssertTrue(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 4, 3), 2)); +} + +- (void)testFuzzyNotEqualRects +{ + CGRect referenceRect = CGRectMake(3, 3, 3, 3); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(5, 5, 5, 5), 1)); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 5, 3, 3), 1)); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(5, 3, 3, 3), 1)); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 3, 5), 1)); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(3, 3, 5, 3), 1)); +} + +- (void)testFuzzyEqualRectsSymmetry +{ + CGRect referenceRect = CGRectMake(0, 0, 2, 2); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(1, 1, 3, 3), 1)); + XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(-1, -1, 1, 1), 1)); +} + +- (void)testSizeInversion +{ + const CGSize screenSizePortrait = CGSizeMake(10, 15); + const CGSize screenSizeLandscape = CGSizeMake(15, 10); + const CGFloat t = FBDefaultFrameFuzzyThreshold; + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizePortrait, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationPortrait), t)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizePortrait, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationPortraitUpsideDown), t)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationLandscapeLeft), t)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizePortrait, UIInterfaceOrientationLandscapeRight), t)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizeLandscape, UIInterfaceOrientationLandscapeLeft), t)); + XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizeLandscape, UIInterfaceOrientationLandscapeRight), t)); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m b/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m new file mode 100644 index 0000000..bc21b1b --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBProtocolHelpersTests.m @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBProtocolHelpers.h" + +@interface FBProtocolHelpersTests : XCTestCase +@end + +@implementation FBProtocolHelpersTests + +- (void)testValidPrefixedCapsParsing +{ + NSError *error = nil; + NSDictionary *parsedCaps = FBParseCapabilities(@{ + @"firstMatch": @[@{ + @"appium:bundleId": @"com.example.id" + }] + }, &error); + XCTAssertNil(error); + XCTAssertEqualObjects(parsedCaps[@"bundleId"], @"com.example.id"); +} + +- (void)testValidPrefixedCapsMerging +{ + NSError *error = nil; + NSDictionary *parsedCaps = FBParseCapabilities(@{ + @"firstMatch": @[@{ + @"bundleId": @"com.example.id" + }], + @"alwaysMatch": @{ + @"google:cap": @"super" + } + }, &error); + XCTAssertNil(error); + XCTAssertEqualObjects(parsedCaps[@"bundleId"], @"com.example.id"); + XCTAssertEqualObjects(parsedCaps[@"google:cap"], @"super"); +} + +- (void)testEmptyCaps +{ + NSError *error = nil; + NSDictionary *parsedCaps = FBParseCapabilities(@{}, &error); + XCTAssertNil(error); + XCTAssertEqual(parsedCaps.count, 0); +} + +- (void)testCapsMergingFailure +{ + NSError *error = nil; + NSDictionary *parsedCaps = FBParseCapabilities(@{ + @"firstMatch": @[@{ + @"appium:bundleId": @"com.example.id" + }], + @"alwaysMatch": @{ + @"bundleId": @"other" + } + }, &error); + XCTAssertNil(parsedCaps); + XCTAssertNotNil(error); +} + +- (void)testPrefixingStandardCapability +{ + NSError *error = nil; + NSDictionary *parsedCaps = FBParseCapabilities(@{ + @"firstMatch": @[@{ + @"appium:platformName": @"com.example.id" + }] + }, &error); + XCTAssertNil(parsedCaps); + XCTAssertNotNil(error); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBRouteTests.m b/WebDriverAgentTests/UnitTests/FBRouteTests.m new file mode 100644 index 0000000..5dede0c --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBRouteTests.m @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBRoute.h" + +@class RouteResponse; + +@interface FBHandlerMock : NSObject +@property (nonatomic, assign) BOOL didCallSomeSelector; +@end + +@implementation FBHandlerMock +- (id)someSelector:(id)arg +{ + self.didCallSomeSelector = YES; + return nil; +}; + +@end + +@interface FBRouteTests : XCTestCase +@end + +@implementation FBRouteTests + +- (void)testGetRoute +{ + FBRoute *route = [FBRoute GET:@"/"]; + XCTAssertEqualObjects(route.verb, @"GET"); +} + +- (void)testPostRoute +{ + FBRoute *route = [FBRoute POST:@"/"]; + XCTAssertEqualObjects(route.verb, @"POST"); +} + +- (void)testPutRoute +{ + FBRoute *route = [FBRoute PUT:@"/"]; + XCTAssertEqualObjects(route.verb, @"PUT"); +} + +- (void)testDeleteRoute +{ + FBRoute *route = [FBRoute DELETE:@"/"]; + XCTAssertEqualObjects(route.verb, @"DELETE"); +} + +- (void)testTargetAction +{ + FBHandlerMock *mock = [FBHandlerMock new]; + FBRoute *route = [[FBRoute new] respondWithTarget:mock action:@selector(someSelector:)]; + [route mountRequest:(id)NSObject.new intoResponse:(id)NSObject.new]; + XCTAssertTrue(mock.didCallSomeSelector); +} + +- (void)testRespond +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"Calling respond block works!"]; + FBRoute *route = [[FBRoute new] respondWithBlock:^id(FBRouteRequest *request) { + [expectation fulfill]; + return nil; + }]; + [route mountRequest:(id)NSObject.new intoResponse:(id)NSObject.new]; + [self waitForExpectationsWithTimeout:0.0 handler:nil]; +} + +- (void)testRouteWithSessionWithSlash +{ + FBRoute *route = [[FBRoute POST:@"/deactivateApp"] respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/session/:sessionID/deactivateApp"); +} + +- (void)testRouteWithSession +{ + FBRoute *route = [[FBRoute POST:@"deactivateApp"] respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/session/:sessionID/deactivateApp"); +} + +- (void)testRouteWithoutSessionWithSlash +{ + FBRoute *route = [[FBRoute POST:@"/deactivateApp"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/deactivateApp"); +} + +- (void)testRouteWithoutSession +{ + FBRoute *route = [[FBRoute POST:@"deactivateApp"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/deactivateApp"); +} + +- (void)testEmptyRouteWithSession +{ + FBRoute *route = [[FBRoute POST:@""] respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/session/:sessionID"); +} + +- (void)testEmptyRouteWithoutSession +{ + FBRoute *route = [[FBRoute POST:@""].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/"); +} + +- (void)testEmptyRouteWithSessionWithSlash +{ + FBRoute *route = [[FBRoute POST:@"/"] respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/session/:sessionID"); +} + +- (void)testEmptyRouteWithoutSessionWithSlash +{ + FBRoute *route = [[FBRoute POST:@"/"].withoutSession respondWithTarget:self action:@selector(dummyHandler:)]; + XCTAssertEqualObjects(route.path, @"/"); +} + ++ (id)dummyHandler:(FBRouteRequest *)request +{ + return nil; +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m b/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m new file mode 100644 index 0000000..da3c415 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBRunLoopSpinnerTests.m @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBRunLoopSpinner.h" + +@interface FBRunLoopSpinnerTests : XCTestCase +@property (nonatomic, strong) FBRunLoopSpinner *spinner; +@end + +/** + Non of the test methods should block testing thread. + If they do, that means they are broken + */ +@implementation FBRunLoopSpinnerTests + +- (void)setUp +{ + [super setUp]; + self.spinner = [[FBRunLoopSpinner new] timeout:0.1]; +} + +- (void)testSpinUntilCompletion +{ + __block BOOL _didExecuteBlock = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void (^completion)(void)) { + _didExecuteBlock = YES; + completion(); + }]; + XCTAssertTrue(_didExecuteBlock); +} + +- (void)testSpinUntilTrue +{ + __block BOOL _didExecuteBlock = NO; + BOOL didSucceed = + [self.spinner spinUntilTrue:^BOOL{ + _didExecuteBlock = YES; + return YES; + }]; + XCTAssertTrue(didSucceed); + XCTAssertTrue(_didExecuteBlock); +} + +- (void)testSpinUntilTrueTimeout +{ + NSError *error; + BOOL didSucceed = + [self.spinner spinUntilTrue:^BOOL{ + return NO; + } error:&error]; + XCTAssertFalse(didSucceed); + XCTAssertNotNil(error); +} + +- (void)testSpinUntilTrueTimeoutMessage +{ + NSString *expectedMessage = @"Magic message"; + NSError *error; + BOOL didSucceed = + [[self.spinner timeoutErrorMessage:expectedMessage] + spinUntilTrue:^BOOL{ + return NO; + } error:&error]; + XCTAssertFalse(didSucceed); + XCTAssertEqual(error.localizedDescription, expectedMessage); +} + +- (void)testSpinUntilNotNil +{ + __block id expectedObject = NSObject.new; + NSError *error; + id returnedObject = + [self.spinner spinUntilNotNil:^id{ + return expectedObject; + } error:&error]; + XCTAssertNil(error); + XCTAssertEqual(returnedObject, expectedObject); +} + +- (void)testSpinUntilNotNilTimeout +{ + NSError *error; + id element = + [self.spinner spinUntilNotNil:^id{ + return nil; + } error:&error]; + XCTAssertNil(element); + XCTAssertNotNil(error); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m b/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m new file mode 100644 index 0000000..e937eca --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBRuntimeUtilsTests.m @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBRuntimeUtils.h" +#import "XCTestPrivateSymbols.h" + +@protocol FBMagicProtocol +@end + +const NSString *FBRuntimeUtilsTestsConstString = @"FBRuntimeUtilsTestsConstString"; + +@interface FBRuntimeUtilsTests : XCTestCase +@end + +@implementation FBRuntimeUtilsTests + +- (void)testClassesThatConformsToProtocol +{ + XCTAssertEqualObjects(@[self.class], FBClassesThatConformsToProtocol(@protocol(FBMagicProtocol))); +} + +- (void)testRetrievingFrameworkSymbols +{ + NSString *binaryPath = [NSBundle bundleForClass:self.class].executablePath; + NSString *symbolPointer = *(NSString*__autoreleasing*)FBRetrieveSymbolFromBinary(binaryPath.UTF8String, "FBRuntimeUtilsTestsConstString"); + XCTAssertNotNil(symbolPointer); + XCTAssertEqualObjects(symbolPointer, FBRuntimeUtilsTestsConstString); +} + +- (void)testXCTestSymbols +{ + XCTAssertTrue(XCDebugLogger != NULL); + XCTAssertTrue(XCSetDebugLogger != NULL); + XCTAssertNotNil(FB_XCAXAIsVisibleAttribute); + XCTAssertNotNil(FB_XCAXAIsElementAttribute); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m b/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m new file mode 100644 index 0000000..666e912 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBSDKVersionTests.m @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBRuntimeUtils.h" + +@interface FBSDKVersionTests : XCTestCase +@property (nonatomic, readonly) NSString *currentSDKVersion; +@property (nonatomic, readonly) NSString *lowerSDKVersion; +@property (nonatomic, readonly) NSString *higherSDKVersion; +@end + +@implementation FBSDKVersionTests + +- (void)setUp +{ + [super setUp]; + NSDictionary *bundleDict = [[NSBundle mainBundle] infoDictionary]; + [bundleDict setValue:@"11.0" forKey:@"DTSDKName"]; + _currentSDKVersion = FBSDKVersion(); + _lowerSDKVersion = [NSString stringWithFormat:@"%@", @((int)[self.currentSDKVersion doubleValue] - 1)]; + _higherSDKVersion = [NSString stringWithFormat:@"%@", @((int)[self.currentSDKVersion doubleValue] + 1)]; +} + +- (void)testIsSDKVersionLessThanOrEqualTo +{ + XCTAssertTrue(isSDKVersionLessThanOrEqualTo(self.higherSDKVersion)); + XCTAssertFalse(isSDKVersionLessThanOrEqualTo(self.lowerSDKVersion)); + XCTAssertTrue(isSDKVersionLessThanOrEqualTo(self.currentSDKVersion)); +} + +- (void)testIsSDKVersionLessThan +{ + XCTAssertTrue(isSDKVersionLessThan(self.higherSDKVersion)); + XCTAssertFalse(isSDKVersionLessThan(self.lowerSDKVersion)); + XCTAssertFalse(isSDKVersionLessThan(self.currentSDKVersion)); +} + +- (void)testIsSDKVersionEqualTo +{ + XCTAssertFalse(isSDKVersionEqualTo(self.higherSDKVersion)); + XCTAssertFalse(isSDKVersionEqualTo(self.lowerSDKVersion)); + XCTAssertTrue(isSDKVersionEqualTo(self.currentSDKVersion)); +} + +- (void)testIsSDKVersionGreaterThanOrEqualTo +{ + XCTAssertFalse(isSDKVersionGreaterThanOrEqualTo(self.higherSDKVersion)); + XCTAssertTrue(isSDKVersionGreaterThanOrEqualTo(self.lowerSDKVersion)); + XCTAssertTrue(isSDKVersionGreaterThanOrEqualTo(self.currentSDKVersion)); +} + +- (void)testIsSDKVersionGreaterThan +{ + XCTAssertFalse(isSDKVersionGreaterThan(self.higherSDKVersion)); + XCTAssertTrue(isSDKVersionGreaterThan(self.lowerSDKVersion)); + XCTAssertFalse(isSDKVersionGreaterThan(self.currentSDKVersion)); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBSessionTests.m b/WebDriverAgentTests/UnitTests/FBSessionTests.m new file mode 100644 index 0000000..53713a3 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBSessionTests.m @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBSession.h" +#import "FBConfiguration.h" +#import "XCUIApplicationDouble.h" + +@interface FBSessionTests : XCTestCase +@property (nonatomic, strong) FBSession *session; +@property (nonatomic, strong) XCUIApplication *testedApplication; +@property (nonatomic) BOOL shouldTerminateAppValue; +@end + +@implementation FBSessionTests + +- (void)setUp +{ + [super setUp]; + self.testedApplication = (id)XCUIApplicationDouble.new; + self.shouldTerminateAppValue = FBConfiguration.shouldTerminateApp; + [FBConfiguration setShouldTerminateApp:NO]; + self.session = [FBSession initWithApplication:self.testedApplication]; +} + +- (void)tearDown +{ + [self.session kill]; + [FBConfiguration setShouldTerminateApp:self.shouldTerminateAppValue]; + [super tearDown]; +} + +- (void)testSessionFetching +{ + FBSession *fetchedSession = [FBSession sessionWithIdentifier:self.session.identifier]; + XCTAssertEqual(self.session, fetchedSession); +} + +- (void)testSessionFetchingBadIdentifier +{ + XCTAssertNil([FBSession sessionWithIdentifier:@"FAKE_IDENTIFIER"]); +} + +- (void)testSessionCreation +{ + XCTAssertNotNil(self.session.identifier); + XCTAssertNotNil(self.session.elementCache); +} + +- (void)testActiveSession +{ + XCTAssertEqual(self.session, [FBSession activeSession]); +} + +- (void)testActiveSessionIsNilAfterKilling +{ + [self.session kill]; + XCTAssertNil([FBSession activeSession]); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m b/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m new file mode 100644 index 0000000..9ddd39b --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBXMLSafeStringTests.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "NSString+FBXMLSafeString.h" + +@interface FBXMLSafeStringTests : XCTestCase +@end + +@implementation FBXMLSafeStringTests + +- (void)testSafeXmlStringTransformationWithEmptyReplacement { + NSString *withInvalidChar = [NSString stringWithFormat:@"bla%@", @"\uFFFF"]; + NSString *withoutInvalidChar = @"bla"; + XCTAssertNotEqualObjects(withInvalidChar, withoutInvalidChar); + XCTAssertEqualObjects([withInvalidChar fb_xmlSafeStringWithReplacement:@""], withoutInvalidChar); +} + +- (void)testSafeXmlStringTransformationWithNonEmptyReplacement { + NSString *withInvalidChar = [NSString stringWithFormat:@"bla%@", @"\uFFFF"]; + XCTAssertEqualObjects([withInvalidChar fb_xmlSafeStringWithReplacement:@"1"], @"bla1"); +} + +- (void)testSafeXmlStringTransformationWithSmileys { + NSString *validString = @"Yo👿"; + XCTAssertEqualObjects([validString fb_xmlSafeStringWithReplacement:@""], validString); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgentTests/UnitTests/FBXPathTests.m new file mode 100644 index 0000000..dbe52e6 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBXPathTests.m @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBMacros.h" +#import "FBXPath.h" +#import "FBXPath-Private.h" +#import "XCUIElementDouble.h" +#import "XCElementSnapshotDouble.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" + +@interface FBXPathTests : XCTestCase +@end + +@implementation FBXPathTests + +- (NSString *)xmlStringWithElement:(id)snapshot + xpathQuery:(nullable NSString *)query + excludingAttributes:(nullable NSArray *)excludedAttributes +{ + xmlDocPtr doc; + + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; + int buffersize; + xmlChar *xmlbuff = NULL; + int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); + if (rc >= 0) { + rc = [FBXPath xmlRepresentationWithRootElement:snapshot + writer:writer + elementStore:elementStore + query:query + excludingAttributes:excludedAttributes]; + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + } + } + if (rc >= 0) { + xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); + } + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + + XCTAssertTrue(rc >= 0); + XCTAssertEqual(1, [elementStore count]); + + NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; + xmlFree(xmlbuff); + return result; +} + +- (void)testDefaultXPathPresentation +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + NSString *resultXml = [self xmlStringWithElement:(id)element + xpathQuery:nil + excludingAttributes:nil]; + NSLog(@"[DefaultXPath] Result XML:\n%@", resultXml); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" traits=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdType, element.wdValue, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, element.wdTraits]; + XCTAssertTrue([resultXml isEqualToString: expectedXml]); +} + +- (void)testtXPathPresentationWithSomeAttributesExcluded +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + NSString *resultXml = [self xmlStringWithElement:(id)element + xpathQuery:nil + excludingAttributes:@[@"type", @"visible", @"value", @"index", @"traits", @"nativeFrame"]]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ name=\"%@\" label=\"%@\" enabled=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"]]; + XCTAssertEqualObjects(resultXml, expectedXml); +} + +- (void)testXPathPresentationBasedOnQueryMatchingAllAttributes +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + snapshot.value = @"йоло<>&\""; + snapshot.label = @"a\nb"; + id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + NSString *resultXml = [self xmlStringWithElement:(id)element + xpathQuery:[NSString stringWithFormat:@"//%@[@*]", element.wdType] + excludingAttributes:@[@"visible"]]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" traits=\"%@\" nativeFrame=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdType, @"йоло<>&"", element.wdName, @"a b", FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, FBBoolToString(element.wdHittable), element.wdTraits, NSStringFromCGRect(element.wdNativeFrame)]; + XCTAssertEqualObjects(expectedXml, resultXml); +} + +- (void)testXPathPresentationBasedOnQueryMatchingSomeAttributes +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + NSString *resultXml = [self xmlStringWithElement:(id)element + xpathQuery:[NSString stringWithFormat:@"//%@[@%@ and contains(@%@, 'blabla')]", element.wdType, @"value", @"name"] + excludingAttributes:nil]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ value=\"%@\" name=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdValue, element.wdName]; + XCTAssertTrue([resultXml isEqualToString: expectedXml]); +} + +- (void)testSnapshotXPathResultsMatching +{ + xmlDocPtr doc; + + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + id root = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + NSString *query = [NSString stringWithFormat:@"//%@", root.wdType]; + int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); + if (rc >= 0) { + rc = [FBXPath xmlRepresentationWithRootElement:(id)root + writer:writer + elementStore:elementStore + query:query + excludingAttributes:nil]; + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + } + } + if (rc < 0) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + XCTFail(@"Unable to create the source XML document"); + } + + xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL]; + if (NULL == queryResult) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + XCTAssertNotEqual(NULL, queryResult); + } + + NSArray *matchingSnapshots = [FBXPath collectMatchingSnapshots:queryResult->nodesetval + elementStore:elementStore]; + xmlXPathFreeObject(queryResult); + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + + XCTAssertNotNil(matchingSnapshots); + XCTAssertEqual(1, [matchingSnapshots count]); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/Info.plist b/WebDriverAgentTests/UnitTests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m b/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m new file mode 100644 index 0000000..6662852 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "NSDictionary+FBUtf8SafeDictionary.h" + +@interface NSDictionaryFBUtf8SafeTests : XCTestCase +@end + +@implementation NSDictionaryFBUtf8SafeTests + +- (void)testEmptySafeDictConversion +{ + NSDictionary *d = @{}; + XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); +} + +- (void)testNonEmptySafeDictConversion +{ + NSDictionary *d = @{ + @"1": @[@3, @4], + @"5": @{@"6": @7, @"8": @9}, + @"10": @"11" + }; + XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m b/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m new file mode 100644 index 0000000..6b7f316 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/NSExpressionFBFormatTests.m @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "NSExpression+FBFormat.h" +#import "FBElementUtils.h" + +@interface NSExpressionFBFormatTests : XCTestCase +@end + +@implementation NSExpressionFBFormatTests + +- (void)testFormattingForExistingProperty +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"wdName"]; + NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; + XCTAssertEqualObjects([prop keyPath], @"wdName"); +} + +- (void)testFormattingForExistingPropertyShortcut +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"visible"]; + NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; + XCTAssertEqualObjects([prop keyPath], @"isWDVisible"); +} + +- (void)testFormattingForValidExpressionWOKeys +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"1"]; + NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; + XCTAssertEqualObjects([prop constantValue], [NSNumber numberWithInt:1]); +} + +- (void)testFormattingForExistingComplexProperty +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"wdRect.x"]; + NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; + XCTAssertEqualObjects([prop keyPath], @"wdRect.x"); +} + +- (void)testFormattingForExistingComplexPropertyWOPrefix +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"rect.x"]; + NSExpression *prop = [NSExpression fb_wdExpressionWithExpression:expr]; + XCTAssertEqualObjects([prop keyPath], @"wdRect.x"); +} + +- (void)testFormattingForPredicateWithUnknownKey +{ + NSExpression *expr = [NSExpression expressionWithFormat:@"title"]; + XCTAssertThrowsSpecificNamed([NSExpression fb_wdExpressionWithExpression:expr], NSException, FBUnknownAttributeException); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m b/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m new file mode 100644 index 0000000..ef01af4 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/NSPredicateFBFormatTests.m @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "NSPredicate+FBFormat.h" + +@interface NSPredicateFBFormatTests : XCTestCase +@end + +@implementation NSPredicateFBFormatTests + +- (void)testFormattingForExistingProperty +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"wdName == 'blabla'"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForExistingPropertyOnTheRightSide +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"0 == wdAccessible"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForExistingPropertyShortcut +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"visible == 1"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForComplexExpression +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"visible == 1 AND NOT (type == 'blabla' OR NOT (label IN {'3', '4'}))"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForValidExpressionWOKeys +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"1 = 1"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForExistingComplexProperty +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"wdRect.x == '0'"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForExistingComplexPropertyWOPrefix +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"rect.x == '0'"]; + XCTAssertNotNil([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testFormattingForPredicateWithUnknownKey +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"title == 'blabla'"]; + XCTAssertThrows([NSPredicate fb_formatSearchPredicate:expr]); +} + +- (void)testBlockPredicateCreation +{ + NSPredicate *expr = [NSPredicate predicateWithFormat:@"rect.x == '0'"]; + XCTAssertNotNil([NSPredicate fb_snapshotBlockPredicateWithPredicate:expr]); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m b/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m new file mode 100644 index 0000000..a12e73f --- /dev/null +++ b/WebDriverAgentTests/UnitTests/XCUIElementHelpersTests.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBElementUtils.h" + +@interface XCUIElementHelpersTests : XCTestCase +@property (nonatomic) NSDictionary *namesMapping; +@end + +@implementation XCUIElementHelpersTests + +- (void)setUp +{ + [super setUp]; + self.namesMapping = [FBElementUtils wdAttributeNamesMapping]; +} + +- (void)testMappingContainsNamesAndAliases +{ + XCTAssertTrue([self.namesMapping.allKeys containsObject:@"wdName"]); + XCTAssertTrue([self.namesMapping.allKeys containsObject:@"name"]); +} + +- (void)testMappingContainsCorrectValueForAttrbutesWithoutGetters +{ + XCTAssertTrue([[self.namesMapping objectForKey:@"label"] isEqualToString:@"wdLabel"]); + XCTAssertTrue([[self.namesMapping objectForKey:@"wdLabel"] isEqualToString:@"wdLabel"]); +} + +- (void)testMappingContainsCorrectValueForAttrbutesWithGetters +{ + XCTAssertTrue([[self.namesMapping objectForKey:@"visible"] isEqualToString:@"isWDVisible"]); + XCTAssertTrue([[self.namesMapping objectForKey:@"wdVisible"] isEqualToString:@"isWDVisible"]); +} + +- (void)testEachPropertyHasAlias +{ + NSArray *aliases = [self.namesMapping.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT(SELF beginsWith[c] 'wd')"]]; + NSArray *names = [self.namesMapping.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF beginsWith[c] 'wd'"]]; + XCTAssertEqual(aliases.count, names.count); +} + +@end diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h new file mode 100644 index 0000000..2a81730 --- /dev/null +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +@class XCUIApplication; + +@interface XCUIElementDouble : NSObject +@property (nonatomic, strong, nonnull) XCUIApplication *application; +@property (nonatomic, readwrite, assign) CGRect frame; +@property (nonatomic, readwrite, nullable) id lastSnapshot; +@property (nonatomic, assign) BOOL fb_isObstructedByAlert; +@property (nonatomic, readwrite, copy, nonnull) NSDictionary *wdRect; +@property (nonatomic, readwrite, assign) CGRect wdFrame; +@property (nonatomic, readwrite, copy, nonnull) NSString *wdUID; +@property (nonatomic, copy, readwrite, nullable) NSString *wdName; +@property (nonatomic, copy, readwrite, nullable) NSString *wdLabel; +@property (nonatomic, copy, readwrite, nonnull) NSString *wdType; +@property (nonatomic, strong, readwrite, nullable) NSString *wdValue; +@property (nonatomic, readwrite, getter=isWDEnabled) BOOL wdEnabled; +@property (nonatomic, readwrite, getter=isWDSelected) BOOL wdSelected; +@property (nonatomic, readwrite) NSUInteger wdIndex; +@property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; +@property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; +@property (nonatomic, readwrite, getter=isWDFocused) BOOL wdFocused; +@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; +@property (copy, nonnull) NSArray *children; +@property (nonatomic, readwrite, assign) XCUIElementType elementType; +@property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; + +- (void)resolve; +- (id _Nonnull)fb_standardSnapshot; +- (id _Nonnull)fb_customSnapshot; + +// Checks +@property (nonatomic, assign, readonly) BOOL didResolve; + +@end diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m new file mode 100644 index 0000000..f3a7914 --- /dev/null +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIElementDouble.h" + +@interface XCUIElementDouble () +@property (nonatomic, assign, readwrite) BOOL didResolve; +@end + +@implementation XCUIElementDouble + +- (id)init +{ + self = [super init]; + if (self) { + self.wdFrame = CGRectMake(0, 0, 0, 0); + self.wdName = @"testName"; + self.wdLabel = @"testLabel"; + self.wdValue = @"magicValue"; + self.wdVisible = YES; + self.wdAccessible = YES; + self.wdEnabled = YES; + self.wdSelected = YES; + self.wdHittable = YES; + self.wdIndex = 0; +#if TARGET_OS_TV + self.wdFocused = YES; +#endif + self.children = @[]; + self.wdRect = @{@"x": @0, + @"y": @0, + @"width": @0, + @"height": @0, + }; + self.wdAccessibilityContainer = NO; + self.elementType = XCUIElementTypeOther; + self.wdType = @"XCUIElementTypeOther"; + self.wdUID = @"0"; + self.lastSnapshot = nil; + } + return self; +} + +- (id)fb_valueForWDAttributeName:(NSString *)name +{ + return @"test"; +} + +- (id)fb_standardSnapshot +{ + return [self lastSnapshot]; +} + +- (id)fb_customSnapshot +{ + return [self lastSnapshot]; +} + +- (void)resolve +{ + self.didResolve = YES; +} + +- (id)lastSnapshot +{ + return self; +} + +- (id)fb_uid +{ + return self.wdUID; +} + +@end diff --git a/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m b/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m new file mode 100644 index 0000000..27fb94d --- /dev/null +++ b/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "XCUIElementDouble.h" +#import "FBTVNavigationTracker.h" +#import "FBTVNavigationTracker-Private.h" + +@interface FBTVNavigationTrackerTests : XCTestCase +@end + +@implementation FBTVNavigationTrackerTests + +- (void)testHorizontalDirectionWithItemShouldBeRight +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:0.1]; + XCTAssertEqual(FBTVDirectionRight, direction); +} + +- (void)testHorizontalDirectionWithItemShouldBeLeft +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:-0.1]; + XCTAssertEqual(FBTVDirectionLeft, direction); +} + +- (void)testHorizontalDirectionWithItemShouldBeNone +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker horizontalDirectionWithItem:item andDelta:DBL_EPSILON]; + XCTAssertEqual(FBTVDirectionNone, direction); +} + +- (void)testVerticalDirectionWithItemShouldBeDown +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:0.1]; + XCTAssertEqual(FBTVDirectionDown, direction); +} + +- (void)testVerticalDirectionWithItemShouldBeUp +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:-0.1]; + XCTAssertEqual(FBTVDirectionUp, direction); +} + +- (void)testVerticalDirectionWithItemShouldBeNone +{ + XCUIElementDouble *el1 = XCUIElementDouble.new; + + FBTVNavigationItem *item = [FBTVNavigationItem itemWithUid:@"123456789"]; + FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:(XCUIElement *)el1]; + + FBTVDirection direction = [tracker verticalDirectionWithItem:item andDelta:DBL_EPSILON]; + XCTAssertEqual(FBTVDirectionNone, direction); +} + +@end diff --git a/WebDriverAgentTests/UnitTests_tvOS/Info.plist b/WebDriverAgentTests/UnitTests_tvOS/Info.plist new file mode 100644 index 0000000..bf5eabc --- /dev/null +++ b/WebDriverAgentTests/UnitTests_tvOS/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.wda.unitTests + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/azure-templates/base_job.yml b/azure-templates/base_job.yml new file mode 100644 index 0000000..e3b62c0 --- /dev/null +++ b/azure-templates/base_job.yml @@ -0,0 +1,36 @@ +parameters: + name: '' + action: '' + target: '' + dest: '' + sdk: '' + iphoneModel: '' + ipadModel: '' + tvModel: '' + iosVersion: '' + xcodeVersion: '' + tvVersion: '' + vmImage: '' + extraXcArgs: '' + + +jobs: + - job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + variables: + ACTION: ${{ parameters.action }} + TARGET: ${{ parameters.target }} + DEST: ${{ parameters.dest }} + SDK: ${{ parameters.sdk }} + CODE_SIGN: ${{ parameters.codeSign }} + IPHONE_MODEL: ${{ parameters.iphoneModel }} + TV_MODEL: ${{ parameters.tvModel }} + IPAD_MODEL: ${{ parameters.ipadModel }} + IOS_VERSION: ${{ parameters.iosVersion }} + XCODE_VERSION: ${{ parameters.xcodeVersion }} + TV_VERSION: ${{ parameters.tvVersion }} + EXTRA_XC_ARGS: ${{ parameters.extraXcArgs }} + steps: + - template: bootstrap_steps.yml + - script: ./Scripts/build.sh diff --git a/azure-templates/bootstrap_steps.yml b/azure-templates/bootstrap_steps.yml new file mode 100644 index 0000000..14c127e --- /dev/null +++ b/azure-templates/bootstrap_steps.yml @@ -0,0 +1,7 @@ +steps: + - script: sudo xcode-select --switch "/Applications/Xcode_$(XCODE_VERSION).app/Contents/Developer" + - script: mkdir -p ./Resources/WebDriverAgent.bundle + - task: UseRubyVersion@0 + inputs: + versionSpec: '3.3' + addToPath: true diff --git a/azure-templates/node_setup_steps.yml b/azure-templates/node_setup_steps.yml new file mode 100644 index 0000000..2da7b6b --- /dev/null +++ b/azure-templates/node_setup_steps.yml @@ -0,0 +1,4 @@ +steps: + - task: NodeTool@0 + inputs: + versionSpec: "$(DEFAULT_NODE_VERSION)" diff --git a/ci-jobs/scripts/azure-print-tag-name.js b/ci-jobs/scripts/azure-print-tag-name.js new file mode 100644 index 0000000..4471529 --- /dev/null +++ b/ci-jobs/scripts/azure-print-tag-name.js @@ -0,0 +1,3 @@ + +const branch = process.env.BUILD_SOURCEBRANCH || ''; +console.log(branch.replace(/^refs\/tags\//, '')); // eslint-disable-line no-console \ No newline at end of file diff --git a/ci-jobs/scripts/build-webdriveragents.js b/ci-jobs/scripts/build-webdriveragents.js new file mode 100644 index 0000000..be7a704 --- /dev/null +++ b/ci-jobs/scripts/build-webdriveragents.js @@ -0,0 +1,57 @@ +const buildWebDriverAgent = require('./build-webdriveragent'); +const { asyncify } = require('asyncbox'); +const { fs, logger } = require('@appium/support'); +const { exec } = require('teen_process'); +const path = require('path'); + +const log = new logger.getLogger('WDABuild'); + +async function buildAndUploadWebDriverAgents () { + // Get all xcode paths from /Applications/ + const xcodePaths = (await fs.readdir('/Applications/')) + .filter((file) => file.toLowerCase().startsWith('xcode_')); + + // Determine which xcodes need to be skipped + let excludedXcodeArr = (process.env.EXCLUDE_XCODE || '').replace(/\s/g, '').split(','); + log.info(`Will skip xcode versions: '${excludedXcodeArr}'`); + + for (let xcodePath of xcodePaths) { + if (xcodePath.includes('beta')) { + log.info(`Skipping beta Xcode '${xcodePath}'`); + continue; + } + + // Skip if .0 because redundant (example: skip 11.4.0 because it already does 11.4) + const [, , patch] = xcodePath.split('.'); + if (patch === '0') { + log.info(`Skipping xcode '${xcodePath}'`); + continue; + } + + // Build webdriveragent for this xcode version + log.info(`Running xcode-select for '${xcodePath}'`); + await exec('sudo', ['xcode-select', '-s', `/Applications/${xcodePath}/Contents/Developer`]); + const xcodeVersion = path.parse(xcodePath).name.split('_', 2)[1]; + + if (excludedXcodeArr.includes(xcodeVersion)) { + log.info(`Skipping xcode version '${xcodeVersion}'`); + continue; + } + + log.info('Building webdriveragent for xcode version', xcodeVersion); + try { + await buildWebDriverAgent(xcodeVersion); + } catch (e) { + log.error(`Skipping build for '${xcodeVersion} due to error: ${e}'`); + } + } + + // Divider log line + log.info('\n'); +} + +if (require.main === module) { + asyncify(buildAndUploadWebDriverAgents); +} + +module.exports = buildAndUploadWebDriverAgents; diff --git a/ci-jobs/templates/build.yml b/ci-jobs/templates/build.yml new file mode 100644 index 0000000..d32d34a --- /dev/null +++ b/ci-jobs/templates/build.yml @@ -0,0 +1,38 @@ +parameters: + vmImage: 'macOS-11' + name: macOS_11 + excludeXcode: $(excludeXcode) +jobs: + - job: ${{ parameters.name }} + variables: + EXCLUDE_XCODE: ${{ parameters.excludeXcode }} + pool: + vmImage: ${{ parameters.vmImage }} + dependsOn: create_github_release + steps: + - script: node ./ci-jobs/scripts/azure-print-tag-name + displayName: Print Tag Name + - script: ls /Applications/ + displayName: List Installed Applications + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: lts/* + - script: npm install + displayName: Install Node Modules + - script: mkdir -p Resources/WebDriverAgent.bundle + displayName: Make Resources Folder + - script: node ./Scripts/build-webdriveragent.js + displayName: Build WebDriverAgents + - script: ls ./bundles + displayName: List WDA Bundles + - task: PublishPipelineArtifact@0 + inputs: + targetPath: bundles/ + artifactName: ${{ parameters.name }} + - script: | + brew install ghr + ghr $(node ./ci-jobs/scripts/azure-print-tag-name) bundles/ + env: + GITHUB_TOKEN: $(GITHUB_TOKEN) + displayName: Upload to GitHub Releases diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..020f8c2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,13 @@ +import appiumConfig from '@appium/eslint-config-appium-ts'; + +export default [ + ...appiumConfig, + { + ignores: [ + 'Configurations/**', + 'Fastlane/**', + 'PrivateHeaders/**', + 'WebDriverAgent*/**' + ], + }, +]; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..d8e3996 --- /dev/null +++ b/index.ts @@ -0,0 +1,7 @@ +export { checkForDependencies, bundleWDASim } from './lib/check-dependencies'; +export { NoSessionProxy } from './lib/no-session-proxy'; +export { WebDriverAgent } from './lib/webdriveragent'; +export { WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE } from './lib/constants'; +export { resetTestProcesses, BOOTSTRAP_PATH } from './lib/utils'; + +export * from './lib/types'; diff --git a/lib/check-dependencies.js b/lib/check-dependencies.js new file mode 100644 index 0000000..4990367 --- /dev/null +++ b/lib/check-dependencies.js @@ -0,0 +1,50 @@ +import { fs } from '@appium/support'; +import _ from 'lodash'; +import { exec } from 'teen_process'; +import path from 'path'; +import {XcodeBuild} from './xcodebuild'; +import * as xcode from 'appium-xcode'; +import { + WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP +} from './constants'; +import { BOOTSTRAP_PATH } from './utils'; +import log from './logger'; + +async function buildWDASim () { + const args = [ + '-project', path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), + '-scheme', WDA_SCHEME, + '-sdk', SDK_SIMULATOR, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED="NO"', + 'GCC_TREAT_WARNINGS_AS_ERRORS=0', + ]; + await exec('xcodebuild', args); +} + +export async function checkForDependencies () { + log.debug('Dependencies are up to date'); + return false; +} + +/** + * + * @param {XcodeBuild} xcodebuild + * @returns {Promise} + */ +export async function bundleWDASim (xcodebuild) { + if (xcodebuild && !_.isFunction(xcodebuild.retrieveDerivedDataPath)) { + xcodebuild = new XcodeBuild(/** @type {import('appium-xcode').XcodeVersion} */ (await xcode.getVersion(true)), {}); + } + + const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); + if (!derivedDataPath) { + throw new Error('Cannot retrieve the path to the Xcode derived data folder'); + } + const wdaBundlePath = path.join(derivedDataPath, 'Build', 'Products', 'Debug-iphonesimulator', WDA_RUNNER_APP); + if (await fs.exists(wdaBundlePath)) { + return wdaBundlePath; + } + await buildWDASim(); + return wdaBundlePath; +} diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..fd6ed48 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,24 @@ +import path from 'path'; + +const DEFAULT_TEST_BUNDLE_SUFFIX = '.xctrunner'; +const WDA_RUNNER_BUNDLE_ID = 'com.facebook.WebDriverAgentRunner'; +const WDA_RUNNER_BUNDLE_ID_FOR_XCTEST = `${WDA_RUNNER_BUNDLE_ID}${DEFAULT_TEST_BUNDLE_SUFFIX}`; +const WDA_RUNNER_APP = 'WebDriverAgentRunner-Runner.app'; +const WDA_SCHEME = 'WebDriverAgentRunner'; +const PROJECT_FILE = 'project.pbxproj'; +const WDA_BASE_URL = 'http://127.0.0.1'; + +const PLATFORM_NAME_TVOS = 'tvOS'; +const PLATFORM_NAME_IOS = 'iOS'; + +const SDK_SIMULATOR = 'iphonesimulator'; +const SDK_DEVICE = 'iphoneos'; + +const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time'); + +export { + WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, PROJECT_FILE, + WDA_SCHEME, PLATFORM_NAME_TVOS, PLATFORM_NAME_IOS, + SDK_SIMULATOR, SDK_DEVICE, WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, + WDA_RUNNER_BUNDLE_ID_FOR_XCTEST, DEFAULT_TEST_BUNDLE_SUFFIX +}; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..4727be0 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,5 @@ +import { logger } from '@appium/support'; + +const log = logger.getLogger('WebDriverAgent'); + +export default log; diff --git a/lib/no-session-proxy.js b/lib/no-session-proxy.js new file mode 100644 index 0000000..f0760c7 --- /dev/null +++ b/lib/no-session-proxy.js @@ -0,0 +1,26 @@ +import { JWProxy } from '@appium/base-driver'; + + +class NoSessionProxy extends JWProxy { + constructor (opts = {}) { + super(opts); + } + + getUrlForProxy (url) { + if (url === '') { + url = '/'; + } + const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`; + let remainingUrl = ''; + if ((new RegExp('^/')).test(url)) { + remainingUrl = url; + } else { + throw new Error(`Did not know what to do with url '${url}'`); + } + remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes + return proxyBase + remainingUrl; + } +} + +export { NoSessionProxy }; +export default NoSessionProxy; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..82c51cb --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,52 @@ +// WebDriverAgentLib/Utilities/FBSettings.h +export interface WDASettings { + elementResponseAttribute?: string; + shouldUseCompactResponses?: boolean; + mjpegServerScreenshotQuality?: number; + mjpegServerFramerate?: number; + screenshotQuality?: number; + elementResponseAttributes?: string; + mjpegScalingFactor?: number; + mjpegFixOrientation?: boolean; + keyboardAutocorrection?: boolean; + keyboardPrediction?: boolean; + customSnapshotTimeout?: number; + snapshotMaxDepth?: number; + useFirstMatch?: boolean; + boundElementsByIndex?: boolean; + reduceMotion?: boolean; + defaultActiveApplication?: string; + activeAppDetectionPoint?: string; + includeNonModalElements?: boolean; + defaultAlertAction?: 'accept' | 'dismiss'; + acceptAlertButtonSelector?: string; + dismissAlertButtonSelector?: string; + screenshotOrientation?: 'auto' | 'portrait' | 'portraitUpsideDown' | 'landscapeRight' | 'landscapeLeft' + waitForIdleTimeout?: number; + animationCoolOffTimeout?: number; + maxTypingFrequency?: number; + useClearTextShortcut?: boolean; +} + +// WebDriverAgentLib/Utilities/FBCapabilities.h +export interface WDACapabilities { + bundleId?: string; + initialUrl?: string; + arguments?: string[]; + environment?: Record; + eventloopIdleDelaySec?: number; + shouldWaitForQuiescence?: boolean; + shouldUseTestManagerForVisibilityDetection?: boolean; + maxTypingFrequency?: number; + shouldUseSingletonTestManager?: boolean; + waitForIdleTimeout?: number; + shouldUseCompactResponses?: number; + elementResponseFields?: unknown; + disableAutomaticScreenshots?: boolean; + shouldTerminateApp?: boolean; + forceAppLaunch?: boolean; + useNativeCachingStrategy?: boolean; + forceSimulatorSoftwareKeyboardPresence?: boolean; + defaultAlertAction?: 'accept' | 'dismiss'; + appLaunchStateTimeoutSec?: number; +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..77ca8f5 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,398 @@ +import { fs, plist } from '@appium/support'; +import { exec } from 'teen_process'; +import path from 'path'; +import log from './logger'; +import _ from 'lodash'; +import { WDA_RUNNER_BUNDLE_ID, PLATFORM_NAME_TVOS } from './constants'; +import B from 'bluebird'; +import _fs from 'fs'; +import { waitForCondition } from 'asyncbox'; +import { arch } from 'os'; + +const PROJECT_FILE = 'project.pbxproj'; + +/** + * Calculates the path to the current module's root folder + * + * @returns {string} The full path to module root + * @throws {Error} If the current module root folder cannot be determined + */ +const getModuleRoot = _.memoize(function getModuleRoot () { + let currentDir = path.dirname(path.resolve(__filename)); + let isAtFsRoot = false; + while (!isAtFsRoot) { + const manifestPath = path.join(currentDir, 'package.json'); + try { + if (_fs.existsSync(manifestPath) && + JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent') { + return currentDir; + } + } catch {} + currentDir = path.dirname(currentDir); + isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; + } + throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); +}); + +export const BOOTSTRAP_PATH = getModuleRoot(); + +async function getPIDsUsingPattern (pattern) { + const args = [ + '-if', // case insensitive, full cmdline match + pattern, + ]; + try { + const {stdout} = await exec('pgrep', args); + return stdout.split(/\s+/) + .map((x) => parseInt(x, 10)) + .filter(_.isInteger) + .map((x) => `${x}`); + } catch (err) { + log.debug(`'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`); + return []; + } +} + +async function killAppUsingPattern (pgrepPattern) { + const signals = [2, 15, 9]; + for (const signal of signals) { + const matchedPids = await getPIDsUsingPattern(pgrepPattern); + if (_.isEmpty(matchedPids)) { + return; + } + const args = [`-${signal}`, ...matchedPids]; + try { + await exec('kill', args); + } catch (err) { + log.debug(`kill ${args.join(' ')} -> ${err.message}`); + } + if (signal === _.last(signals)) { + // there is no need to wait after SIGKILL + return; + } + try { + await waitForCondition(async () => { + const pidCheckPromises = matchedPids + .map((pid) => exec('kill', ['-0', pid]) + // the process is still alive + .then(() => false) + // the process is dead + .catch(() => true) + ); + return (await B.all(pidCheckPromises)) + .every((x) => x === true); + }, { + waitMs: 1000, + intervalMs: 100, + }); + return; + } catch { + // try the next signal + } + } +} + +/** + * Return true if the platformName is tvOS + * @param {string} platformName The name of the platorm + * @returns {boolean} Return true if the platformName is tvOS + */ +function isTvOS (platformName) { + return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); +} + +async function replaceInFile (file, find, replace) { + let contents = await fs.readFile(file, 'utf8'); + + let newContents = contents.replace(find, replace); + if (newContents !== contents) { + await fs.writeFile(file, newContents, 'utf8'); + } +} + +/** + * Update WebDriverAgentRunner project bundle ID with newBundleId. + * This method assumes project file is in the correct state. + * @param {string} agentPath - Path to the .xcodeproj directory. + * @param {string} newBundleId the new bundle ID used to update. + */ +async function updateProjectFile (agentPath, newBundleId) { + let projectFilePath = path.resolve(agentPath, PROJECT_FILE); + try { + // Assuming projectFilePath is in the correct state, create .old from projectFilePath + await fs.copyFile(projectFilePath, `${projectFilePath}.old`); + await replaceInFile(projectFilePath, new RegExp(_.escapeRegExp(WDA_RUNNER_BUNDLE_ID), 'g'), newBundleId); + log.debug(`Successfully updated '${projectFilePath}' with bundle id '${newBundleId}'`); + } catch (err) { + log.debug(`Error updating project file: ${err.message}`); + log.warn(`Unable to update project file '${projectFilePath}' with ` + + `bundle id '${newBundleId}'. WebDriverAgent may not start`); + } +} + +/** + * Reset WebDriverAgentRunner project bundle ID to correct state. + * @param {string} agentPath - Path to the .xcodeproj directory. + */ +async function resetProjectFile (agentPath) { + const projectFilePath = path.join(agentPath, PROJECT_FILE); + try { + // restore projectFilePath from .old file + if (!await fs.exists(`${projectFilePath}.old`)) { + return; // no need to reset + } + await fs.mv(`${projectFilePath}.old`, projectFilePath); + log.debug(`Successfully reset '${projectFilePath}' with bundle id '${WDA_RUNNER_BUNDLE_ID}'`); + } catch (err) { + log.debug(`Error resetting project file: ${err.message}`); + log.warn(`Unable to reset project file '${projectFilePath}' with ` + + `bundle id '${WDA_RUNNER_BUNDLE_ID}'. WebDriverAgent has been ` + + `modified and not returned to the original state.`); + } +} + +async function setRealDeviceSecurity (keychainPath, keychainPassword) { + log.debug('Setting security for iOS device'); + await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); + await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); + await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); +} + +/** + * Information of the device under test + * @typedef {Object} DeviceInfo + * @property {string} isRealDevice - Equals to true if the current device is a real device + * @property {string} udid - The device UDID. + * @property {string} platformVersion - The platform version of OS. + * @property {string} platformName - The platform name of iOS, tvOS +*/ +/** + * Creates xctestrun file per device & platform version. + * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-${x86_64|arm64}.xctestrun for simulator located @bootstrapPath + * Newer Xcode (Xcode 10.0 at least) generate xctestrun file following sdkVersion. + * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun + * even if the cap has platform version 11.4 + * + * @param {DeviceInfo} deviceInfo + * @param {string} sdkVersion - The Xcode SDK version of OS. + * @param {string} bootstrapPath - The folder path containing xctestrun file. + * @param {number|string} wdaRemotePort - The remote port WDA is listening on. + * @return {Promise} returns xctestrunFilePath for given device + * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, + * then it will throw file not found exception + */ +async function setXctestrunFile (deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort) { + const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); + const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); + const updateWDAPort = getAdditionalRunContent(deviceInfo.platformName, wdaRemotePort); + const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); + await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); + + return xctestrunFilePath; +} + +/** + * Return the WDA object which appends existing xctest runner content + * @param {string} platformName - The name of the platform + * @param {number|string} wdaRemotePort - The remote port number + * @return {object} returns a runner object which has USE_PORT + */ +function getAdditionalRunContent (platformName, wdaRemotePort) { + const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; + + return { + [runner]: { + EnvironmentVariables: { + // USE_PORT must be 'string' + USE_PORT: `${wdaRemotePort}` + } + } + }; +} + +/** + * Return the path of xctestrun if it exists + * @param {DeviceInfo} deviceInfo + * @param {string} sdkVersion - The Xcode SDK version of OS. + * @param {string} bootstrapPath - The folder path containing xctestrun file. + * @returns {Promise} + */ +async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { + // First try the SDK path, for Xcode 10 (at least) + const sdkBased = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), + sdkVersion, + ]; + // Next try Platform path, for earlier Xcode versions + const platformBased = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`), + deviceInfo.platformVersion, + ]; + + for (const [filePath, version] of [sdkBased, platformBased]) { + if (await fs.exists(filePath)) { + log.info(`Using '${filePath}' as xctestrun file`); + return filePath; + } + const originalXctestrunFile = path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, version)); + if (await fs.exists(originalXctestrunFile)) { + // If this is first time run for given device, then first generate xctestrun file for device. + // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices. + await fs.copyFile(originalXctestrunFile, filePath); + log.info(`Using '${filePath}' as xctestrun file copied by '${originalXctestrunFile}'`); + return filePath; + } + } + + throw new Error( + `If you are using 'useXctestrunFile' capability then you ` + + `need to have a xctestrun file (expected: ` + + `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')` + ); +} + + +/** + * Return the name of xctestrun file + * @param {DeviceInfo} deviceInfo + * @param {string} version - The Xcode SDK version of OS. + * @return {string} returns xctestrunFilePath for given device + */ +function getXctestrunFileName (deviceInfo, version) { + const archSuffix = deviceInfo.isRealDevice + ? `os${version}-arm64` + : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`; + return `WebDriverAgentRunner_${isTvOS(deviceInfo.platformName) ? 'tvOS_appletv' : 'iphone'}${archSuffix}.xctestrun`; +} + +/** + * Ensures the process is killed after the timeout + * + * @param {string} name + * @param {import('teen_process').SubProcess} proc + * @returns {Promise} + */ +async function killProcess (name, proc) { + if (!proc || !proc.isRunning) { + return; + } + + log.info(`Shutting down '${name}' process (pid '${proc.proc?.pid}')`); + + log.info(`Sending 'SIGTERM'...`); + try { + await proc.stop('SIGTERM', 1000); + return; + } catch (err) { + if (!err.message.includes(`Process didn't end after`)) { + throw err; + } + log.debug(`${name} process did not end in a timely fashion: '${err.message}'.`); + } + + log.info(`Sending 'SIGKILL'...`); + try { + await proc.stop('SIGKILL'); + } catch (err) { + if (err.message.includes('not currently running')) { + // the process ended but for some reason we were not informed + return; + } + throw err; + } +} + +/** + * Generate a random integer. + * + * @return {number} A random integer number in range [low, hight). `low`` is inclusive and `high` is exclusive. + */ +function randomInt (low, high) { + return Math.floor(Math.random() * (high - low) + low); +} + +/** + * Retrieves WDA upgrade timestamp + * + * @return {Promise} The UNIX timestamp of the package manifest. The manifest only gets modified on + * package upgrade. + */ +async function getWDAUpgradeTimestamp () { + const packageManifest = path.resolve(getModuleRoot(), 'package.json'); + if (!await fs.exists(packageManifest)) { + return null; + } + const {mtime} = await fs.stat(packageManifest); + return mtime.getTime(); +} + +/** + * Kills running XCTest processes for the particular device. + * + * @param {string} udid - The device UDID. + * @param {boolean} isSimulator - Equals to true if the current device is a Simulator + */ +async function resetTestProcesses (udid, isSimulator) { + const processPatterns = [`xcodebuild.*${udid}`]; + if (isSimulator) { + processPatterns.push(`${udid}.*XCTRunner`); + // The pattern to find in case idb was used + processPatterns.push(`xctest.*${udid}`); + } + log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); + await B.all(processPatterns.map(killAppUsingPattern)); +} + +/** + * Get the IDs of processes listening on the particular system port. + * It is also possible to apply additional filtering based on the + * process command line. + * + * @param {string|number} port - The port number. + * @param {?Function} filteringFunc - Optional lambda function, which + * receives command line string of the particular process + * listening on given port, and is expected to return + * either true or false to include/exclude the corresponding PID + * from the resulting array. + * @returns {Promise} - the list of matched process ids. + */ +async function getPIDsListeningOnPort (port, filteringFunc = null) { + const result = []; + try { + // This only works since Mac OS X El Capitan + const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]); + result.push(...(stdout.trim().split(/\n+/))); + } catch (e) { + if (e.code !== 1) { + // code 1 means no processes. Other errors need reporting + log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`); + } + return result; + } + + if (!_.isFunction(filteringFunc)) { + return result; + } + return await B.filter(result, async (pid) => { + let stdout; + try { + ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); + } catch (e) { + if (e.code === 1) { + // The process does not exist anymore, there's nothing to filter + return false; + } + throw e; + } + return await filteringFunc(stdout); + }); +} + +export { updateProjectFile, resetProjectFile, setRealDeviceSecurity, + getAdditionalRunContent, getXctestrunFileName, + setXctestrunFile, getXctestrunFilePath, killProcess, randomInt, + getWDAUpgradeTimestamp, resetTestProcesses, + getPIDsListeningOnPort, killAppUsingPattern, isTvOS +}; diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js new file mode 100644 index 0000000..6d9ab3f --- /dev/null +++ b/lib/webdriveragent.js @@ -0,0 +1,741 @@ +import { waitForCondition } from 'asyncbox'; +import _ from 'lodash'; +import path from 'path'; +import url from 'url'; +import B from 'bluebird'; +import { JWProxy } from '@appium/base-driver'; +import { fs, util, plist } from '@appium/support'; +import defaultLogger from './logger'; +import { NoSessionProxy } from './no-session-proxy'; +import { + getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH +} from './utils'; +import {XcodeBuild} from './xcodebuild'; +import AsyncLock from 'async-lock'; +import { exec } from 'teen_process'; +import { bundleWDASim } from './check-dependencies'; +import { + WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, + WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, DEFAULT_TEST_BUNDLE_SUFFIX +} from './constants'; +import {Xctest} from 'appium-ios-device'; +import {strongbox} from '@appium/strongbox'; + +const WDA_LAUNCH_TIMEOUT = 60 * 1000; +const WDA_AGENT_PORT = 8100; +const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; +const SHARED_RESOURCES_GUARD = new AsyncLock(); +const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; + +export class WebDriverAgent { + /** @type {string} */ + bootstrapPath; + + /** @type {string} */ + agentPath; + + /** + * @param {import('appium-xcode').XcodeVersion} xcodeVersion + * // TODO: make args typed + * @param {import('@appium/types').StringRecord} [args={}] + * @param {import('@appium/types').AppiumLogger?} [log=null] + */ + constructor (xcodeVersion, args = {}, log = null) { + this.xcodeVersion = xcodeVersion; + + this.args = _.clone(args); + this.log = log ?? defaultLogger; + + this.device = args.device; + this.platformVersion = args.platformVersion; + this.platformName = args.platformName; + this.iosSdkVersion = args.iosSdkVersion; + this.host = args.host; + this.isRealDevice = !!args.realDevice; + this.idb = (args.device || {}).idb; + this.wdaBundlePath = args.wdaBundlePath; + + this.setWDAPaths(args.bootstrapPath, args.agentPath); + + this.wdaLocalPort = args.wdaLocalPort; + this.wdaRemotePort = ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort) + || WDA_AGENT_PORT; + this.wdaBaseUrl = args.wdaBaseUrl || WDA_BASE_URL; + + this.prebuildWDA = args.prebuildWDA; + + // this.args.webDriverAgentUrl guiarantees the capabilities acually + // gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl' + // could be used for caching WDA with xcodebuild. + this.webDriverAgentUrl = args.webDriverAgentUrl; + + this.started = false; + + this.wdaConnectionTimeout = args.wdaConnectionTimeout; + + this.useXctestrunFile = args.useXctestrunFile; + this.usePrebuiltWDA = args.usePrebuiltWDA; + this.derivedDataPath = args.derivedDataPath; + this.mjpegServerPort = args.mjpegServerPort; + + this.updatedWDABundleId = args.updatedWDABundleId; + + this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; + this.usePreinstalledWDA = args.usePreinstalledWDA; + this.xctestApiClient = null; + this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; + + this.xcodebuild = this.canSkipXcodebuild + ? null + : new XcodeBuild(this.xcodeVersion, this.device, { + platformVersion: this.platformVersion, + platformName: this.platformName, + iosSdkVersion: this.iosSdkVersion, + agentPath: this.agentPath, + bootstrapPath: this.bootstrapPath, + realDevice: this.isRealDevice, + showXcodeLog: args.showXcodeLog, + xcodeConfigFile: args.xcodeConfigFile, + xcodeOrgId: args.xcodeOrgId, + xcodeSigningId: args.xcodeSigningId, + keychainPath: args.keychainPath, + keychainPassword: args.keychainPassword, + useSimpleBuildTest: args.useSimpleBuildTest, + usePrebuiltWDA: args.usePrebuiltWDA, + updatedWDABundleId: this.updatedWDABundleId, + launchTimeout: this.wdaLaunchTimeout, + wdaRemotePort: this.wdaRemotePort, + useXctestrunFile: this.useXctestrunFile, + derivedDataPath: args.derivedDataPath, + mjpegServerPort: this.mjpegServerPort, + allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, + resultBundlePath: args.resultBundlePath, + resultBundleVersion: args.resultBundleVersion, + }, this.log); + } + + /** + * Return true if the session does not need xcodebuild. + * @returns {boolean} Whether the session needs/has xcodebuild. + */ + get canSkipXcodebuild () { + // Use this.args.webDriverAgentUrl to guarantee + // the capabilities set gave the `appium:webDriverAgentUrl`. + return this.usePreinstalledWDA || this.args.webDriverAgentUrl; + } + + /** + * Return bundle id for WebDriverAgent to launch the WDA. + * The primary usage is with 'this.usePreinstalledWDA'. + * It adds `.xctrunner` as suffix by default but 'this.updatedWDABundleIdSuffix' + * lets skip it. + * + * @returns {string} Bundle ID for Xctest. + */ + get bundleIdForXctest () { + return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; + } + + /** + * @param {string} [bootstrapPath] + * @param {string} [agentPath] + */ + setWDAPaths (bootstrapPath, agentPath) { + // allow the user to specify a place for WDA. This is undocumented and + // only here for the purposes of testing development of WDA + this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH; + this.log.info(`Using WDA path: '${this.bootstrapPath}'`); + + // for backward compatibility we need to be able to specify agentPath too + this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj'); + this.log.info(`Using WDA agent: '${this.agentPath}'`); + } + + /** + * @returns {Promise} + */ + async cleanupObsoleteProcesses () { + const obsoletePids = await getPIDsListeningOnPort(/** @type {string} */ (this.url.port), + (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') && + !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase())); + + if (_.isEmpty(obsoletePids)) { + this.log.debug(`No obsolete cached processes from previous WDA sessions ` + + `listening on port ${this.url.port} have been found`); + return; + } + + this.log.info(`Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` + + `from previous WDA sessions. Cleaning them up`); + try { + await exec('kill', obsoletePids); + } catch (e) { + this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + + `Original error: ${e.message}`); + } + } + + /** + * Return boolean if WDA is running or not + * @return {Promise} True if WDA is running + * @throws {Error} If there was invalid response code or body + */ + async isRunning () { + return !!(await this.getStatus()); + } + + /** + * @returns {string} + */ + get basePath () { + if (this.url.path === '/') { + return ''; + } + return this.url.path || ''; + } + + /** + * Return current running WDA's status like below + * { + * "state": "success", + * "os": { + * "name": "iOS", + * "version": "11.4", + * "sdkVersion": "11.3" + * }, + * "ios": { + * "simulatorVersion": "11.4", + * "ip": "172.254.99.34" + * }, + * "build": { + * "time": "Jun 24 2018 17:08:21", + * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" + * } + * } + * + * @param {number} [timeoutMs=0] If the given timeoutMs is zero or negative number, + * this function will return the response of `/status` immediately. If the given timeoutMs, + * this function will try to get the response of `/status` up to the timeoutMs. + * @return {Promise} State Object + * @throws {Error} If there was an error within timeoutMs timeout. + * No error is raised if zero or negative number for the timeoutMs. + */ + async getStatus (timeoutMs = 0) { + const noSessionProxy = new NoSessionProxy({ + server: this.url.hostname, + port: this.url.port, + base: this.basePath, + timeout: 3000, + }); + + const sendGetStatus = async () => await /** @type import('@appium/types').StringRecord */ (noSessionProxy.command('/status', 'GET')); + + if (_.isNil(timeoutMs) || timeoutMs <= 0) { + try { + return await sendGetStatus(); + } catch (err) { + this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`); + return null; + } + } + + let lastError = null; + let status = null; + try { + await waitForCondition(async () => { + try { + status = await sendGetStatus(); + return true; + } catch (err) { + lastError = err; + } + return false; + }, { + waitMs: timeoutMs, + intervalMs: 300, + }); + } catch (err) { + this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` + + `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`); + throw new Error(`WDA was not ready in ${timeoutMs} ms.`); + } + return status; + } + + /** + * Uninstall WDAs from the test device. + * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA. + * Appium does not expect multiple WDAs are running on a device. + * + * @returns {Promise} + */ + async uninstall () { + try { + const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); + if (_.isEmpty(bundleIds)) { + this.log.debug('No WDAs on the device.'); + return; + } + + this.log.debug(`Uninstalling WDAs: '${bundleIds}'`); + for (const bundleId of bundleIds) { + await this.device.removeApp(bundleId); + } + } catch (e) { + this.log.debug(e); + this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + + `Original error: ${e.message}`); + } + } + + async _cleanupProjectIfFresh () { + if (this.canSkipXcodebuild) { + return; + } + + const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); + const box = strongbox(packageInfo.name); + let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); + if (!boxItem) { + const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); + if (await fs.exists(timestampPath)) { + // TODO: It is probably a bit ugly to hardcode the recent version string, + // TODO: hovewer it should do the job as a temporary transition trick + // TODO: to switch from a hardcoded file path to the strongbox usage. + try { + boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0'); + } catch (e) { + this.log.warn(`The actual module version cannot be persisted: ${e.message}`); + return; + } + } else { + this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); + try { + await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version); + } catch (e) { + this.log.warn(`The actual module version cannot be persisted: ${e.message}`); + } + return; + } + } + + let recentModuleVersion = await boxItem.read(); + try { + recentModuleVersion = util.coerceVersion(recentModuleVersion, true); + } catch (e) { + this.log.warn(`The persisted module version string has been damaged: ${e.message}`); + this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`); + await boxItem.write(packageInfo.version); + return; + } + + if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) { + this.log.info( + `WebDriverAgent does not need a cleanup. The project sources are up to date ` + + `(${recentModuleVersion} >= ${packageInfo.version})` + ); + return; + } + + this.log.info( + `Cleaning up the WebDriverAgent project after the module upgrade has happened ` + + `(${recentModuleVersion} < ${packageInfo.version})` + ); + try { + // @ts-ignore xcodebuild should be set + await this.xcodebuild.cleanProject(); + await boxItem.write(packageInfo.version); + } catch (e) { + this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`); + } + } + + + /** + * @typedef {Object} LaunchWdaViaDeviceCtlOptions + * @property {Record} [env] environment variables for the launching WDA process + */ + + /** + * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'. + * The WDA package must be prepared properly like published via + * https://github.com/appium/WebDriverAgent/releases + * with proper sign for this case. + * + * When we implement launching XCTest service via appium-ios-device, + * this implementation can be replaced with it. + * + * @param {LaunchWdaViaDeviceCtlOptions} [opts={}] launching WDA with devicectl command options. + * @return {Promise} + */ + async _launchViaDevicectl(opts = {}) { + const {env} = opts; + + await this.device.devicectl.launchApp( + this.bundleIdForXctest, { env, terminateExisting: true } + ); + } + + /** + * Launch WDA with preinstalled package without xcodebuild. + * @param {string} sessionId Launch WDA and establish the session with this sessionId + * @return {Promise} State Object + * @throws {Error} If there was an error within timeoutMs timeout. + * No error is raised if zero or negative number for the timeoutMs. + */ + async launchWithPreinstalledWDA(sessionId) { + const xctestEnv = { + USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, + WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest + }; + if (this.mjpegServerPort) { + xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; + } + this.log.info('Launching WebDriverAgent on the device without xcodebuild'); + if (this.isRealDevice) { + // Current method to launch WDA process can be done via 'xcrun devicectl', + // but it has limitation about the WDA preinstalled package. + // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 + if (util.compareVersions(this.platformVersion, '>=', '17.0')) { + await this._launchViaDevicectl({env: xctestEnv}); + } else { + this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv}); + await this.xctestApiClient.start(); + } + } else { + await this.device.simctl.exec('launch', { + args: [ + '--terminate-running-process', + this.device.udid, + this.bundleIdForXctest, + ], + env: xctestEnv, + }); + } + + this.setupProxies(sessionId); + let status; + try { + status = await this.getStatus(this.wdaLaunchTimeout); + } catch { + throw new Error( + `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` + + `The WebDriverAgent might not be properly built or the device might be locked. ` + + `The 'appium:wdaLaunchTimeout' capability modifies the timeout.` + ); + } + this.started = true; + return status; + } + + /** + * Return current running WDA's status like below after launching WDA + * { + * "state": "success", + * "os": { + * "name": "iOS", + * "version": "11.4", + * "sdkVersion": "11.3" + * }, + * "ios": { + * "simulatorVersion": "11.4", + * "ip": "172.254.99.34" + * }, + * "build": { + * "time": "Jun 24 2018 17:08:21", + * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" + * } + * } + * + * @param {string} sessionId Launch WDA and establish the session with this sessionId + * @return {Promise} State Object + * @throws {Error} If there was invalid response code or body + */ + async launch (sessionId) { + if (this.webDriverAgentUrl) { + this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`); + this.url = this.webDriverAgentUrl; + this.setupProxies(sessionId); + return await this.getStatus(); + } + + if (this.usePreinstalledWDA) { + return await this.launchWithPreinstalledWDA(sessionId); + } + + this.log.info('Launching WebDriverAgent on the device'); + + this.setupProxies(sessionId); + + if (!this.useXctestrunFile && !await fs.exists(this.agentPath)) { + throw new Error(`Trying to use WebDriverAgent project at '${this.agentPath}' but the ` + + 'file does not exist'); + } + + // useXctestrunFile and usePrebuiltWDA use existing dependencies + // It depends on user side + if (this.idb || this.useXctestrunFile || this.usePrebuiltWDA) { + this.log.info('Skipped WDA project cleanup according to the provided capabilities'); + } else { + const synchronizationKey = path.normalize(this.bootstrapPath); + await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, + async () => await this._cleanupProjectIfFresh()); + } + + // We need to provide WDA local port, because it might be occupied + await resetTestProcesses(this.device.udid, !this.isRealDevice); + + if (this.idb) { + return await this.startWithIDB(); + } + + // @ts-ignore xcodebuild should be set + await this.xcodebuild.init(this.noSessionProxy); + + // Start the xcodebuild process + if (this.prebuildWDA) { + // @ts-ignore xcodebuild should be set + await this.xcodebuild.prebuild(); + } + // @ts-ignore xcodebuild should be set + return await this.xcodebuild.start(); + } + + /** + * @returns {Promise} + */ + async startWithIDB () { + this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled'); + const {wdaBundleId, testBundleId} = await this.prepareWDA(); + const env = { + USE_PORT: this.wdaRemotePort, + WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest, + }; + if (this.mjpegServerPort) { + env.MJPEG_SERVER_PORT = this.mjpegServerPort; + } + + return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env}); + } + + /** + * + * @param {string} wdaBundlePath + * @returns {Promise} + */ + async parseBundleId (wdaBundlePath) { + const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); + const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath)); + if (!infoPlist.CFBundleIdentifier) { + throw new Error(`Could not find bundle id in '${infoPlistPath}'`); + } + return infoPlist.CFBundleIdentifier; + } + + /** + * @returns {Promise<{wdaBundleId: string, testBundleId: string, wdaBundlePath: string}>} + */ + async prepareWDA () { + const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle(); + const wdaBundleId = await this.parseBundleId(wdaBundlePath); + if (!await this.device.isAppInstalled(wdaBundleId)) { + await this.device.installApp(wdaBundlePath); + } + const testBundleId = await this.idb.installXCTestBundle(path.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest')); + return {wdaBundleId, testBundleId, wdaBundlePath}; + } + + /** + * @returns {Promise} + */ + async fetchWDABundle () { + if (!this.derivedDataPath) { + return await bundleWDASim(/** @type {XcodeBuild} */ (this.xcodebuild)); + } + const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { + absolute: true, + }); + if (_.isEmpty(wdaBundlePaths)) { + throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); + } + return wdaBundlePaths[0]; + } + + /** + * @returns {Promise} + */ + async isSourceFresh () { + const existsPromises = [ + 'Resources', + `Resources${path.sep}WebDriverAgent.bundle`, + ].map((subPath) => fs.exists(path.resolve(/** @type {String} */ (this.bootstrapPath), subPath))); + return (await B.all(existsPromises)).some((v) => v === false); + } + + /** + * @param {string} sessionId + * @returns {void} + */ + setupProxies (sessionId) { + const proxyOpts = { + log: this.log, + server: this.url.hostname ?? undefined, + port: parseInt(this.url.port ?? '', 10) || undefined, + base: this.basePath, + timeout: this.wdaConnectionTimeout, + keepAlive: true, + scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', + }; + if (this.args.reqBasePath) { + proxyOpts.reqBasePath = this.args.reqBasePath; + } + + this.jwproxy = new JWProxy(proxyOpts); + this.jwproxy.sessionId = sessionId; + this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); + + this.noSessionProxy = new NoSessionProxy(proxyOpts); + } + + /** + * @returns {Promise} + */ + async quit () { + if (this.usePreinstalledWDA) { + this.log.info('Stopping the XCTest session'); + if (this.xctestApiClient) { + this.xctestApiClient.stop(); + this.xctestApiClient = null; + } else { + try { + await this.device.simctl.terminateApp(this.bundleIdForXctest); + } catch (e) { + this.log.warn(e.message); + } + } + } else if (!this.args.webDriverAgentUrl) { + this.log.info('Shutting down sub-processes'); + await this.xcodebuild?.quit(); + await this.xcodebuild?.reset(); + } else { + this.log.debug('Do not stop xcodebuild nor XCTest session ' + + 'since the WDA session is managed by outside this driver.'); + } + + if (this.jwproxy) { + this.jwproxy.sessionId = null; + } + + this.started = false; + + if (!this.args.webDriverAgentUrl) { + // if we populated the url ourselves (during `setupCaching` call, for instance) + // then clean that up. If the url was supplied, we want to keep it + this.webDriverAgentUrl = null; + } + } + + /** + * @returns {import('url').UrlWithStringQuery} + */ + get url () { + if (!this._url) { + if (this.webDriverAgentUrl) { + this._url = url.parse(this.webDriverAgentUrl); + } else { + const port = this.wdaLocalPort || WDA_AGENT_PORT; + const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL); + this._url = url.parse(`${protocol}//${hostname}:${port}`); + } + } + return this._url; + } + + /** + * @param {string} _url + * @returns {void} + */ + set url (_url) { + this._url = url.parse(_url); + } + + /** + * @returns {boolean} + */ + get fullyStarted () { + return this.started; + } + + /** + * @param {boolean} started + * @returns {void}s + */ + set fullyStarted (started) { + this.started = started ?? false; + } + + /** + * @returns {Promise} + */ + async retrieveDerivedDataPath () { + if (this.canSkipXcodebuild) { + return; + } + return await /** @type {XcodeBuild} */ (this.xcodebuild).retrieveDerivedDataPath(); + } + + /** + * Reuse running WDA if it has the same bundle id with updatedWDABundleId. + * Or reuse it if it has the default id without updatedWDABundleId. + * Uninstall it if the method faces an exception for the above situation. + * @returns {Promise} + */ + async setupCaching () { + const status = await this.getStatus(); + if (!status || !status.build) { + this.log.debug('WDA is currently not running. There is nothing to cache'); + return; + } + + const { + productBundleIdentifier, + upgradedAt, + } = status.build; + // for real device + if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) { + this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`); + return await this.uninstall(); + } + // for simulator + if (util.hasValue(productBundleIdentifier) && !util.hasValue(this.updatedWDABundleId) && WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier) { + this.log.info(`Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`); + return await this.uninstall(); + } + + const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); + this.log.debug(`Upgrade timestamp of the currently bundled WDA: ${actualUpgradeTimestamp}`); + this.log.debug(`Upgrade timestamp of the WDA on the device: ${upgradedAt}`); + if (actualUpgradeTimestamp && upgradedAt && _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`)) { + this.log.info('Will uninstall running WDA since it has different version in comparison to the one ' + + `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`); + return await this.uninstall(); + } + + const message = util.hasValue(productBundleIdentifier) + ? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'` + : `Will reuse previously cached WDA instance at '${this.url.href}'`; + this.log.info(`${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`); + this.webDriverAgentUrl = this.url.href; + } + + /** + * Quit and uninstall running WDA. + * @returns {Promise} + */ + async quitAndUninstall () { + await this.quit(); + await this.uninstall(); + } +} + +export default WebDriverAgent; diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js new file mode 100644 index 0000000..041d24e --- /dev/null +++ b/lib/xcodebuild.js @@ -0,0 +1,466 @@ +import { retryInterval } from 'asyncbox'; +import { SubProcess, exec } from 'teen_process'; +import { logger, timing } from '@appium/support'; +import defaultLogger from './logger'; +import B from 'bluebird'; +import { + setRealDeviceSecurity, setXctestrunFile, + updateProjectFile, resetProjectFile, killProcess, + getWDAUpgradeTimestamp, isTvOS +} from './utils'; +import _ from 'lodash'; +import path from 'path'; +import { WDA_RUNNER_BUNDLE_ID } from './constants'; + + +const DEFAULT_SIGNING_ID = 'iPhone Developer'; +const PREBUILD_DELAY = 0; +const RUNNER_SCHEME_IOS = 'WebDriverAgentRunner'; +const LIB_SCHEME_IOS = 'WebDriverAgentLib'; + +const ERROR_WRITING_ATTACHMENT = 'Error writing attachment data to file'; +const ERROR_COPYING_ATTACHMENT = 'Error copying testing attachment'; +const IGNORED_ERRORS = [ + ERROR_WRITING_ATTACHMENT, + ERROR_COPYING_ATTACHMENT, + 'Failed to remove screenshot at path', +]; +const IGNORED_ERRORS_PATTERN = new RegExp( + '(' + + IGNORED_ERRORS + .map((errStr) => _.escapeRegExp(errStr)) + .join('|') + + ')' +); + +const RUNNER_SCHEME_TV = 'WebDriverAgentRunner_tvOS'; +const LIB_SCHEME_TV = 'WebDriverAgentLib_tvOS'; + +const REAL_DEVICES_CONFIG_DOCS_LINK = 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/'; + +const xcodeLog = logger.getLogger('Xcode'); + + +export class XcodeBuild { + /** @type {SubProcess} */ + xcodebuild; + + /** + * @param {import('appium-xcode').XcodeVersion} xcodeVersion + * @param {any} device + * // TODO: make args typed + * @param {import('@appium/types').StringRecord} [args={}] + * @param {import('@appium/types').AppiumLogger?} [log=null] + */ + constructor (xcodeVersion, device, args = {}, log = null) { + this.xcodeVersion = xcodeVersion; + + this.device = device; + this.log = log ?? defaultLogger; + + this.realDevice = args.realDevice; + + this.agentPath = args.agentPath; + this.bootstrapPath = args.bootstrapPath; + + this.platformVersion = args.platformVersion; + this.platformName = args.platformName; + this.iosSdkVersion = args.iosSdkVersion; + + this.showXcodeLog = args.showXcodeLog; + + this.xcodeConfigFile = args.xcodeConfigFile; + this.xcodeOrgId = args.xcodeOrgId; + this.xcodeSigningId = args.xcodeSigningId || DEFAULT_SIGNING_ID; + this.keychainPath = args.keychainPath; + this.keychainPassword = args.keychainPassword; + + this.prebuildWDA = args.prebuildWDA; + this.usePrebuiltWDA = args.usePrebuiltWDA; + this.useSimpleBuildTest = args.useSimpleBuildTest; + + this.useXctestrunFile = args.useXctestrunFile; + + this.launchTimeout = args.launchTimeout; + + this.wdaRemotePort = args.wdaRemotePort; + + this.updatedWDABundleId = args.updatedWDABundleId; + this.derivedDataPath = args.derivedDataPath; + + this.mjpegServerPort = args.mjpegServerPort; + + this.prebuildDelay = _.isNumber(args.prebuildDelay) ? args.prebuildDelay : PREBUILD_DELAY; + + this.allowProvisioningDeviceRegistration = args.allowProvisioningDeviceRegistration; + + this.resultBundlePath = args.resultBundlePath; + this.resultBundleVersion = args.resultBundleVersion; + + this._didBuildFail = false; + this._didProcessExit = false; + } + + /** + * + * @param {any} noSessionProxy + * @returns {Promise} + */ + async init (noSessionProxy) { + this.noSessionProxy = noSessionProxy; + + if (this.useXctestrunFile) { + const deviveInfo = { + isRealDevice: this.realDevice, + udid: this.device.udid, + platformVersion: this.platformVersion, + platformName: this.platformName + }; + this.xctestrunFilePath = await setXctestrunFile(deviveInfo, this.iosSdkVersion, this.bootstrapPath, this.wdaRemotePort); + return; + } + + // if necessary, update the bundleId to user's specification + if (this.realDevice) { + // In case the project still has the user specific bundle ID, reset the project file first. + // - We do this reset even if updatedWDABundleId is not specified, + // since the previous updatedWDABundleId test has generated the user specific bundle ID project file. + // - We don't call resetProjectFile for simulator, + // since simulator test run will work with any user specific bundle ID. + await resetProjectFile(this.agentPath); + if (this.updatedWDABundleId) { + await updateProjectFile(this.agentPath, this.updatedWDABundleId); + } + } + } + + /** + * @returns {Promise} + */ + async retrieveDerivedDataPath () { + if (this.derivedDataPath) { + return this.derivedDataPath; + } + + // avoid race conditions + if (this._derivedDataPathPromise) { + return await this._derivedDataPathPromise; + } + + this._derivedDataPathPromise = (async () => { + let stdout; + try { + ({stdout} = await exec('xcodebuild', ['-project', this.agentPath, '-showBuildSettings'])); + } catch (err) { + this.log.warn(`Cannot retrieve WDA build settings. Original error: ${err.message}`); + return; + } + + const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m; + const match = pattern.exec(stdout); + if (!match) { + this.log.warn(`Cannot parse WDA build dir from ${_.truncate(stdout, {length: 300})}`); + return; + } + this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`); + // Derived data root is two levels higher over the build dir + this.derivedDataPath = path.dirname(path.dirname(path.normalize(match[1]))); + this.log.debug(`Got derived data root: '${this.derivedDataPath}'`); + return this.derivedDataPath; + })(); + return await this._derivedDataPathPromise; + } + + /** + * @returns {Promise} + */ + async reset () { + // if necessary, reset the bundleId to original value + if (this.realDevice && this.updatedWDABundleId) { + await resetProjectFile(this.agentPath); + } + } + + /** + * @returns {Promise} + */ + async prebuild () { + // first do a build phase + this.log.debug('Pre-building WDA before launching test'); + this.usePrebuiltWDA = true; + await this.start(true); + + if (this.prebuildDelay > 0) { + // pause a moment + await B.delay(this.prebuildDelay); + } + } + + /** + * @returns {Promise} + */ + async cleanProject () { + const libScheme = isTvOS(this.platformName) ? LIB_SCHEME_TV : LIB_SCHEME_IOS; + const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; + + for (const scheme of [libScheme, runnerScheme]) { + this.log.debug(`Cleaning the project scheme '${scheme}' to make sure there are no leftovers from previous installs`); + await exec('xcodebuild', [ + 'clean', + '-project', this.agentPath, + '-scheme', scheme, + ]); + } + } + + /** + * + * @param {boolean} [buildOnly=false] + * @returns {{cmd: string, args: string[]}} + */ + getCommand (buildOnly = false) { + const cmd = 'xcodebuild'; + /** @type {string[]} */ + const args = []; + + // figure out the targets for xcodebuild + const [buildCmd, testCmd] = this.useSimpleBuildTest ? ['build', 'test'] : ['build-for-testing', 'test-without-building']; + if (buildOnly) { + args.push(buildCmd); + } else if (this.usePrebuiltWDA || this.useXctestrunFile) { + args.push(testCmd); + } else { + args.push(buildCmd, testCmd); + } + + if (this.allowProvisioningDeviceRegistration) { + // To -allowProvisioningDeviceRegistration flag takes effect, -allowProvisioningUpdates needs to be passed as well. + args.push('-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration'); + } + + if (this.resultBundlePath) { + args.push('-resultBundlePath', this.resultBundlePath); + } + + if (this.resultBundleVersion) { + args.push('-resultBundleVersion', this.resultBundleVersion); + } + + if (this.useXctestrunFile && this.xctestrunFilePath) { + args.push('-xctestrun', this.xctestrunFilePath); + } else { + const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; + args.push('-project', this.agentPath, '-scheme', runnerScheme); + if (this.derivedDataPath) { + args.push('-derivedDataPath', this.derivedDataPath); + } + } + args.push('-destination', `id=${this.device.udid}`); + + const versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion); + if (versionMatch) { + args.push( + `${isTvOS(this.platformName) ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}` + ); + } else { + this.log.warn(`Cannot parse major and minor version numbers from platformVersion "${this.platformVersion}". ` + + 'Will build for the default platform instead'); + } + + if (this.realDevice) { + if (this.xcodeConfigFile) { + this.log.debug(`Using Xcode configuration file: '${this.xcodeConfigFile}'`); + args.push('-xcconfig', this.xcodeConfigFile); + } + if (this.xcodeOrgId && this.xcodeSigningId) { + args.push( + `DEVELOPMENT_TEAM=${this.xcodeOrgId}`, + `CODE_SIGN_IDENTITY=${this.xcodeSigningId}`, + ); + } + } + + if (!process.env.APPIUM_XCUITEST_TREAT_WARNINGS_AS_ERRORS) { + // This sometimes helps to survive Xcode updates + args.push('GCC_TREAT_WARNINGS_AS_ERRORS=0'); + } + + // Below option slightly reduces build time in debug build + // with preventing to generate `/Index/DataStore` which is used by development + args.push('COMPILER_INDEX_STORE_ENABLE=NO'); + + return {cmd, args}; + } + + /** + * @param {boolean} [buildOnly=false] + * @returns {Promise} + */ + async createSubProcess (buildOnly = false) { + if (!this.useXctestrunFile && this.realDevice) { + if (this.keychainPath && this.keychainPassword) { + await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); + } + } + + const {cmd, args} = this.getCommand(buildOnly); + this.log.debug(`Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + + `in directory '${this.bootstrapPath}'`); + /** @type {Record} */ + const env = Object.assign({}, process.env, { + USE_PORT: this.wdaRemotePort, + WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, + }); + if (this.mjpegServerPort) { + // https://github.com/appium/WebDriverAgent/pull/105 + env.MJPEG_SERVER_PORT = this.mjpegServerPort; + } + const upgradeTimestamp = await getWDAUpgradeTimestamp(); + if (upgradeTimestamp) { + env.UPGRADE_TIMESTAMP = upgradeTimestamp; + } + this._didBuildFail = false; + const xcodebuild = new SubProcess(cmd, args, { + cwd: this.bootstrapPath, + env, + detached: true, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let logXcodeOutput = !!this.showXcodeLog; + const logMsg = _.isBoolean(this.showXcodeLog) + ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` + : 'Output from xcodebuild will only be logged if any errors are present there'; + this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); + + const onStreamLine = (/** @type {string} */ line) => { + if (this.showXcodeLog === false || IGNORED_ERRORS_PATTERN.test(line)) { + return; + } + // if we have an error we want to output the logs + // otherwise the failure is inscrutible + // but do not log permission errors from trying to write to attachments folder + if (line.includes('Error Domain=')) { + logXcodeOutput = true; + // handle case where xcode returns 0 but is failing + this._didBuildFail = true; + } + if (logXcodeOutput) { + xcodeLog.info(line); + } + }; + for (const streamName of ['stderr', 'stdout']) { + xcodebuild.on(`line-${streamName}`, onStreamLine); + } + + return xcodebuild; + } + + + /** + * @param {boolean} [buildOnly=false] + * @returns {Promise} + */ + async start (buildOnly = false) { + this.xcodebuild = await this.createSubProcess(buildOnly); + + // wrap the start procedure in a promise so that we can catch, and report, + // any startup errors that are thrown as events + return await new B((resolve, reject) => { + this.xcodebuild.once('exit', (code, signal) => { + xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); + this.xcodebuild.removeAllListeners(); + this.didProcessExit = true; + if (this._didBuildFail || (!signal && code !== 0)) { + let errorMessage = `xcodebuild failed with code ${code}.` + + ` This usually indicates an issue with the local Xcode setup or WebDriverAgent` + + ` project configuration or the driver-to-platform version mismatch.`; + if (!this.showXcodeLog) { + errorMessage += ` Consider setting 'showXcodeLog' capability to true in` + + ` order to check the Appium server log for build-related error messages.`; + } else if (this.realDevice) { + errorMessage += ` Consider checking the WebDriverAgent configuration guide` + + ` for real iOS devices at ${REAL_DEVICES_CONFIG_DOCS_LINK}.`; + } + return reject(new Error(errorMessage)); + } + // in the case of just building, the process will exit and that is our finish + if (buildOnly) { + return resolve(); + } + }); + + return (async () => { + try { + const timer = new timing.Timer().start(); + await this.xcodebuild.start(true); + if (!buildOnly) { + resolve(/** @type {import('@appium/types').StringRecord} */ (await this.waitForStart(timer))); + } + } catch (err) { + let msg = `Unable to start WebDriverAgent: ${err}`; + this.log.error(msg); + reject(new Error(msg)); + } + })(); + }); + } + + /** + * + * @param {any} timer + * @returns {Promise} + */ + async waitForStart (timer) { + // try to connect once every 0.5 seconds, until `launchTimeout` is up + this.log.debug(`Waiting up to ${this.launchTimeout}ms for WebDriverAgent to start`); + let currentStatus = null; + try { + const retries = Math.trunc(this.launchTimeout / 500); + await retryInterval(retries, 1000, async () => { + if (this._didProcessExit) { + // there has been an error elsewhere and we need to short-circuit + return currentStatus; + } + + const proxyTimeout = this.noSessionProxy.timeout; + this.noSessionProxy.timeout = 1000; + try { + currentStatus = await this.noSessionProxy.command('/status', 'GET'); + if (currentStatus && currentStatus.ios && currentStatus.ios.ip) { + this.agentUrl = currentStatus.ios.ip; + } + this.log.debug(`WebDriverAgent information:`); + this.log.debug(JSON.stringify(currentStatus, null, 2)); + } catch (err) { + throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`); + } finally { + this.noSessionProxy.timeout = proxyTimeout; + } + }); + + if (this._didProcessExit) { + // there has been an error elsewhere and we need to short-circuit + return currentStatus; + } + + this.log.debug(`WebDriverAgent successfully started after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); + } catch (err) { + this.log.debug(err.stack); + throw new Error( + `We were not able to retrieve the /status response from the WebDriverAgent server after ${this.launchTimeout}ms timeout.` + + `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.` + ); + } + return currentStatus; + } + + /** + * @returns {Promise} + */ + async quit () { + await killProcess('xcodebuild', this.xcodebuild); + } +} + +export default XcodeBuild; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7cfbe46 --- /dev/null +++ b/package.json @@ -0,0 +1,105 @@ +{ + "name": "appium-webdriveragent", + "version": "10.1.2", + "description": "Package bundling WebDriverAgent", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "scripts": { + "build": "tsc -b", + "dev": "npm run build -- --watch", + "clean": "npm run build -- --clean", + "lint": "eslint .", + "format": "prettier -w ./lib", + "lint:fix": "npm run lint -- --fix", + "prepare": "npm run build", + "version": "npm run sync-wda-version", + "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"", + "e2e-test": "mocha --exit --timeout 10m \"./test/functional/**/*-specs.js\"", + "bundle": "npm run bundle:ios && npm run bundle:tv", + "bundle:ios": "TARGET=runner SDK=sim node ./Scripts/build-webdriveragent.js", + "bundle:tv": "TARGET=tv_runner SDK=tv_sim node ./Scripts/build-webdriveragent.js", + "fetch-prebuilt-wda": "node ./Scripts/fetch-prebuilt-wda.js", + "sync-wda-version": "node ./scripts/update-wda-version.js --package-version=${npm_package_version} && git add WebDriverAgentLib/Info.plist" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" + }, + "prettier": { + "bracketSpacing": false, + "printWidth": 100, + "singleQuote": true + }, + "repository": { + "type": "git", + "url": "git+https://github.com/appium/WebDriverAgent.git" + }, + "keywords": [ + "Appium", + "iOS", + "WebDriver", + "Selenium", + "WebDriverAgent" + ], + "author": "Appium Contributors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/appium/WebDriverAgent/issues" + }, + "homepage": "https://github.com/appium/WebDriverAgent#readme", + "devDependencies": { + "@appium/eslint-config-appium-ts": "^2.0.0-rc.1", + "@appium/test-support": "^4.0.0-rc.1", + "@appium/tsconfig": "^1.0.0-rc.1", + "@appium/types": "^1.0.0-rc.1", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/git": "^10.0.1", + "@types/bluebird": "^3.5.38", + "@types/lodash": "^4.14.196", + "@types/mocha": "^10.0.1", + "@types/node": "^24.0.0", + "appium-xcode": "^6.0.0", + "chai": "^6.0.0", + "chai-as-promised": "^8.0.0", + "conventional-changelog-conventionalcommits": "^9.0.0", + "node-simctl": "^8.0.0", + "mocha": "^11.0.1", + "prettier": "^3.0.0", + "semantic-release": "^24.0.0", + "semver": "^7.3.7", + "sinon": "^21.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.4.2" + }, + "dependencies": { + "@appium/base-driver": "^10.0.0-rc.1", + "@appium/strongbox": "^1.0.0-rc.1", + "@appium/support": "^7.0.0-rc.1", + "appium-ios-device": "^3.0.0", + "appium-ios-simulator": "^7.0.0", + "async-lock": "^1.0.0", + "asyncbox": "^3.0.0", + "axios": "^1.4.0", + "bluebird": "^3.5.5", + "lodash": "^4.17.11", + "source-map-support": "^0.x", + "teen_process": "^3.0.0" + }, + "files": [ + "index.ts", + "lib", + "build/index.*", + "build/lib", + "Scripts/build.sh", + "Scripts/fetch-prebuilt-wda.js", + "Scripts/build-webdriveragent.js", + "Configurations", + "PrivateHeaders", + "WebDriverAgent.xcodeproj", + "WebDriverAgentLib", + "WebDriverAgentRunner", + "WebDriverAgentTests", + "XCTWebDriverAgentLib", + "CHANGELOG.md" + ] +} diff --git a/test/functional/desired.js b/test/functional/desired.js new file mode 100644 index 0000000..52a3d19 --- /dev/null +++ b/test/functional/desired.js @@ -0,0 +1,5 @@ +import { util } from '@appium/support'; + +export const PLATFORM_VERSION = process.env.PLATFORM_VERSION ? process.env.PLATFORM_VERSION : '11.3'; +export const DEVICE_NAME = process.env.DEVICE_NAME + || (util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone X' : 'iPhone 6'); diff --git a/test/functional/helpers/simulator.js b/test/functional/helpers/simulator.js new file mode 100644 index 0000000..f315f20 --- /dev/null +++ b/test/functional/helpers/simulator.js @@ -0,0 +1,41 @@ +import _ from 'lodash'; +import { Simctl } from 'node-simctl'; +import { retryInterval } from 'asyncbox'; +import { killAllSimulators as simKill } from 'appium-ios-simulator'; +import { resetTestProcesses } from '../../../lib/utils'; + + +async function killAllSimulators () { + if (process.env.CLOUD) { + return; + } + + const simctl = new Simctl(); + const allDevices = _.flatMap(_.values(await simctl.getDevices())); + const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); + + for (const {udid} of bootedDevices) { + // It is necessary to stop the corresponding xcodebuild process before killing + // the simulator, otherwise it will be automatically restarted + await resetTestProcesses(udid, true); + simctl.udid = udid; + await simctl.shutdownDevice(); + } + await simKill(); +} + +async function shutdownSimulator (device) { + // stop XCTest processes if running to avoid unexpected side effects + await resetTestProcesses(device.udid, true); + await device.shutdown(); +} + +async function deleteDeviceWithRetry (udid) { + const simctl = new Simctl({udid}); + try { + await retryInterval(10, 1000, simctl.deleteDevice.bind(simctl)); + } catch {} +} + + +export { killAllSimulators, shutdownSimulator, deleteDeviceWithRetry }; diff --git a/test/functional/webdriveragent-e2e-specs.js b/test/functional/webdriveragent-e2e-specs.js new file mode 100644 index 0000000..bb1ce67 --- /dev/null +++ b/test/functional/webdriveragent-e2e-specs.js @@ -0,0 +1,129 @@ +import { Simctl } from 'node-simctl'; +import { getVersion } from 'appium-xcode'; +import { getSimulator } from 'appium-ios-simulator'; +import { killAllSimulators, shutdownSimulator } from './helpers/simulator'; +import { SubProcess } from 'teen_process'; +import { PLATFORM_VERSION, DEVICE_NAME } from './desired'; +import { retryInterval } from 'asyncbox'; +import { WebDriverAgent } from '../../lib/webdriveragent'; +import axios from 'axios'; + +const MOCHA_TIMEOUT_MS = 60 * 1000 * 5; + +const SIM_DEVICE_NAME = 'webDriverAgentTest'; +const SIM_STARTUP_TIMEOUT_MS = MOCHA_TIMEOUT_MS; + +let testUrl = 'http://localhost:8100/tree'; + +function getStartOpts (device) { + return { + device, + platformVersion: PLATFORM_VERSION, + host: 'localhost', + port: 8100, + realDevice: false, + showXcodeLog: true, + wdaLaunchTimeout: 60 * 3 * 1000, + }; +} + + +describe('WebDriverAgent', function () { + this.timeout(MOCHA_TIMEOUT_MS); + let chai; + let xcodeVersion; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + + // Don't do these tests on Sauce Labs + if (process.env.CLOUD) { + this.skip(); + } + + xcodeVersion = await getVersion(true); + }); + describe('with fresh sim', function () { + let device; + let simctl; + + before(async function () { + simctl = new Simctl(); + simctl.udid = await simctl.createDevice( + SIM_DEVICE_NAME, + DEVICE_NAME, + PLATFORM_VERSION + ); + device = await getSimulator(simctl.udid); + + // Prebuild WDA + const wda = new WebDriverAgent(xcodeVersion, { + iosSdkVersion: PLATFORM_VERSION, + platformVersion: PLATFORM_VERSION, + showXcodeLog: true, + device, + }); + await wda.xcodebuild.start(true); + }); + + after(async function () { + this.timeout(MOCHA_TIMEOUT_MS); + + await shutdownSimulator(device); + + await simctl.deleteDevice(); + }); + + describe('with running sim', function () { + this.timeout(6 * 60 * 1000); + beforeEach(async function () { + await killAllSimulators(); + await device.run({startupTimeout: SIM_STARTUP_TIMEOUT_MS}); + }); + afterEach(async function () { + try { + await retryInterval(5, 1000, async function () { + await shutdownSimulator(device); + }); + } catch {} + }); + + it('should launch agent on a sim', async function () { + const agent = new WebDriverAgent(xcodeVersion, getStartOpts(device)); + + await agent.launch('sessionId'); + await axios({url: testUrl}).should.be.eventually.rejected; + await agent.quit(); + }); + + it('should fail if xcodebuild fails', async function () { + // short timeout + this.timeout(35 * 1000); + + const agent = new WebDriverAgent(xcodeVersion, getStartOpts(device)); + + agent.xcodebuild.createSubProcess = async function () { + let args = [ + '-workspace', + `${this.agentPath}dfgs`, + // '-scheme', + // 'XCTUITestRunner', + // '-destination', + // `id=${this.device.udid}`, + // 'test' + ]; + return new SubProcess('xcodebuild', args, {detached: true}); + }; + + await agent.launch('sessionId') + .should.eventually.be.rejectedWith('xcodebuild failed'); + + await agent.quit(); + }); + }); + }); +}); diff --git a/test/unit/utils-specs.js b/test/unit/utils-specs.js new file mode 100644 index 0000000..c8002ea --- /dev/null +++ b/test/unit/utils-specs.js @@ -0,0 +1,172 @@ +import { getXctestrunFilePath, getAdditionalRunContent, getXctestrunFileName } from '../../lib/utils'; +import { PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS } from '../../lib/constants'; +import { withMocks } from '@appium/test-support'; +import { fs } from '@appium/support'; +import path from 'path'; +import { fail } from 'assert'; +import { arch } from 'os'; + +function get_arch() { + return arch() === 'arm64' ? 'arm64' : 'x86_64'; +} + +describe('utils', function () { + let chai; + + before(async function() { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + describe('#getXctestrunFilePath', withMocks({fs}, function (mocks) { + const platformVersion = '12.0'; + const sdkVersion = '12.2'; + const udid = 'xxxxxyyyyyyzzzzzz'; + const bootstrapPath = 'path/to/data'; + const platformName = PLATFORM_NAME_IOS; + + afterEach(function () { + mocks.verify(); + }); + + it('should return sdk based path with udid', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .never(); + const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + }); + + it('should return sdk based path without udid, copy them', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .withExactArgs( + path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`), + path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`) + ) + .returns(true); + const deviceInfo = {isRealDevice: true, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + }); + + it('should return platform based path', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .never(); + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + }); + + it('should return platform based path without udid, copy them', async function () { + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .returns(false); + mocks.fs.expects('exists') + .withExactArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`)) + .returns(true); + mocks.fs.expects('copyFile') + .withExactArgs( + path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`), + path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`) + ) + .returns(true); + + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath) + .should.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + }); + + it('should raise an exception because of no files', async function () { + const expected = path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`); + mocks.fs.expects('exists').exactly(4).returns(false); + + const deviceInfo = {isRealDevice: false, udid, platformVersion}; + try { + await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); + fail(); + } catch (err) { + err.message.should.equal(`If you are using 'useXctestrunFile' capability then you need to have a xctestrun file (expected: '${expected}')`); + } + }); + })); + + describe('#getAdditionalRunContent', function () { + it('should return ios format', function () { + const wdaPort = getAdditionalRunContent(PLATFORM_NAME_IOS, 8000); + wdaPort.WebDriverAgentRunner + .EnvironmentVariables.USE_PORT + .should.equal('8000'); + }); + + it('should return tvos format', function () { + const wdaPort = getAdditionalRunContent(PLATFORM_NAME_TVOS, '9000'); + wdaPort.WebDriverAgentRunner_tvOS + .EnvironmentVariables.USE_PORT + .should.equal('9000'); + }); + }); + + describe('#getXctestrunFileName', function () { + const platformVersion = '12.0'; + const udid = 'xxxxxyyyyyyzzzzzz'; + + it('should return ios format, real device', function () { + const platformName = 'iOs'; + const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; + + getXctestrunFileName(deviceInfo, '10.2.0').should.equal( + 'WebDriverAgentRunner_iphoneos10.2.0-arm64.xctestrun'); + }); + + it('should return ios format, simulator', function () { + const platformName = 'ios'; + const deviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; + + getXctestrunFileName(deviceInfo, '10.2.0').should.equal( + `WebDriverAgentRunner_iphonesimulator10.2.0-${get_arch()}.xctestrun`); + }); + + it('should return tvos format, real device', function () { + const platformName = 'tVos'; + const deviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; + + getXctestrunFileName(deviceInfo, '10.2.0').should.equal( + 'WebDriverAgentRunner_tvOS_appletvos10.2.0-arm64.xctestrun'); + }); + + it('should return tvos format, simulator', function () { + const platformName = 'tvOS'; + const deviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; + + getXctestrunFileName(deviceInfo, '10.2.0').should.equal( + `WebDriverAgentRunner_tvOS_appletvsimulator10.2.0-${get_arch()}.xctestrun`); + }); + }); +}); diff --git a/test/unit/webdriveragent-specs.js b/test/unit/webdriveragent-specs.js new file mode 100644 index 0000000..8c00390 --- /dev/null +++ b/test/unit/webdriveragent-specs.js @@ -0,0 +1,412 @@ +import { BOOTSTRAP_PATH } from '../../lib/utils'; +import { WebDriverAgent } from '../../lib/webdriveragent'; +import * as utils from '../../lib/utils'; +import path from 'path'; +import _ from 'lodash'; +import sinon from 'sinon'; + +const fakeConstructorArgs = { + device: 'some sim', + platformVersion: '9', + host: 'me', + port: '5000', + realDevice: false +}; + +const defaultAgentPath = path.resolve(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'); +const customBootstrapPath = '/path/to/wda'; +const customAgentPath = '/path/to/some/agent/WebDriverAgent.xcodeproj'; +const customDerivedDataPath = '/path/to/some/agent/DerivedData/'; + +describe('Constructor', function () { + let chai; + + before(async function() { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + it('should have a default wda agent if not specified', function () { + let agent = new WebDriverAgent({}, fakeConstructorArgs); + agent.bootstrapPath.should.eql(BOOTSTRAP_PATH); + agent.agentPath.should.eql(defaultAgentPath); + }); + it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { + let agent = new WebDriverAgent({}, _.defaults({ + bootstrapPath: customBootstrapPath, + }, fakeConstructorArgs)); + agent.bootstrapPath.should.eql(customBootstrapPath); + agent.agentPath.should.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); + }); + it('should have custom wda bootstrap and agent if both specified', function () { + let agent = new WebDriverAgent({}, _.defaults({ + bootstrapPath: customBootstrapPath, + agentPath: customAgentPath, + }, fakeConstructorArgs)); + agent.bootstrapPath.should.eql(customBootstrapPath); + agent.agentPath.should.eql(customAgentPath); + }); + it('should have custom derivedDataPath if specified', function () { + let agent = new WebDriverAgent({}, _.defaults({ + derivedDataPath: customDerivedDataPath + }, fakeConstructorArgs)); + agent.xcodebuild.derivedDataPath.should.eql(customDerivedDataPath); + }); +}); + +describe('launch', function () { + it('should use webDriverAgentUrl override and return current status', async function () { + const override = 'http://mockurl:8100/'; + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = override; + const agent = new WebDriverAgent({}, args); + const wdaStub = sinon.stub(agent, 'getStatus'); + wdaStub.callsFake(function () { + return {build: 'data'}; + }); + + await agent.launch('sessionId').should.eventually.eql({build: 'data'}); + agent.url.href.should.eql(override); + agent.jwproxy.server.should.eql('mockurl'); + agent.jwproxy.port.should.eql(8100); + agent.jwproxy.base.should.eql(''); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.server.should.eql('mockurl'); + agent.noSessionProxy.port.should.eql(8100); + agent.noSessionProxy.base.should.eql(''); + agent.noSessionProxy.scheme.should.eql('http'); + wdaStub.reset(); + }); +}); + +describe('use wda proxy url', function () { + it('should use webDriverAgentUrl wda proxy url', async function () { + const override = 'http://127.0.0.1:8100/aabbccdd'; + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = override; + const agent = new WebDriverAgent({}, args); + const wdaStub = sinon.stub(agent, 'getStatus'); + wdaStub.callsFake(function () { + return {build: 'data'}; + }); + + await agent.launch('sessionId').should.eventually.eql({build: 'data'}); + + agent.url.port.should.eql('8100'); + agent.url.hostname.should.eql('127.0.0.1'); + agent.url.path.should.eql('/aabbccdd'); + agent.jwproxy.server.should.eql('127.0.0.1'); + agent.jwproxy.port.should.eql(8100); + agent.jwproxy.base.should.eql('/aabbccdd'); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.server.should.eql('127.0.0.1'); + agent.noSessionProxy.port.should.eql(8100); + agent.noSessionProxy.base.should.eql('/aabbccdd'); + agent.noSessionProxy.scheme.should.eql('http'); + }); +}); + +describe('get url', function () { + it('should use default WDA listening url', function () { + const args = Object.assign({}, fakeConstructorArgs); + const agent = new WebDriverAgent({}, args); + agent.url.href.should.eql('http://127.0.0.1:8100/'); + agent.setupProxies('mysession'); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.scheme.should.eql('http'); + }); + it('should use default WDA listening url with emply base url', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = ''; + + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = wdaLocalPort; + + const agent = new WebDriverAgent({}, args); + agent.url.href.should.eql('http://127.0.0.1:9100/'); + agent.setupProxies('mysession'); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.scheme.should.eql('http'); + }); + it('should use customised WDA listening url', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = 'http://mockurl'; + + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = wdaLocalPort; + + const agent = new WebDriverAgent({}, args); + agent.url.href.should.eql('http://mockurl:9100/'); + agent.setupProxies('mysession'); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.scheme.should.eql('http'); + }); + it('should use customised WDA listening url with slash', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = 'http://mockurl/'; + + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = wdaLocalPort; + + const agent = new WebDriverAgent({}, args); + agent.url.href.should.eql('http://mockurl:9100/'); + agent.setupProxies('mysession'); + agent.jwproxy.scheme.should.eql('http'); + agent.noSessionProxy.scheme.should.eql('http'); + }); + it('should use the given webDriverAgentUrl and ignore other params', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = 'http://mockurl/'; + args.wdaLocalPort = '9100'; + args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; + + const agent = new WebDriverAgent({}, args); + agent.url.href.should.eql('https://127.0.0.1:8100/'); + }); + it('should set scheme to https for https webDriverAgentUrl', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; + const agent = new WebDriverAgent({}, args); + agent.setupProxies('mysession'); + agent.jwproxy.scheme.should.eql('https'); + agent.noSessionProxy.scheme.should.eql('https'); + }); +}); + +describe('setupCaching()', function () { + let wda; + let wdaStub; + let wdaStubUninstall; + const getTimestampStub = sinon.stub(utils, 'getWDAUpgradeTimestamp'); + + beforeEach(function () { + wda = new WebDriverAgent('1'); + wdaStub = sinon.stub(wda, 'getStatus'); + wdaStubUninstall = sinon.stub(wda, 'uninstall'); + }); + + afterEach(function () { + for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { + if (stub) { + stub.reset(); + } + } + }); + + it('should not call uninstall since no Running WDA', async function () { + wdaStub.callsFake(function () { + return null; + }); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + _.isUndefined(wda.webDriverAgentUrl).should.be.true; + }); + + it('should not call uninstall since running WDA has only time', async function () { + wdaStub.callsFake(function () { + return {build: { time: 'Jun 24 2018 17:08:21' }}; + }); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + wda.webDriverAgentUrl.should.equal('http://127.0.0.1:8100/'); + }); + + it('should call uninstall once since bundle id is not default without updatedWDABundleId capability', async function () { + wdaStub.callsFake(function () { + return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; + }); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.calledOnce.should.be.true; + _.isUndefined(wda.webDriverAgentUrl).should.be.true; + }); + + it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { + wdaStub.callsFake(function () { + return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.different.WebDriverAgent' }}; + }); + + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.calledOnce.should.be.true; + _.isUndefined(wda.webDriverAgentUrl).should.be.true; + }); + + it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { + wda = new WebDriverAgent('1', { updatedWDABundleId: 'com.example.WebDriverAgent' }); + wdaStub = sinon.stub(wda, 'getStatus'); + wdaStubUninstall = sinon.stub(wda, 'uninstall'); + + wdaStub.callsFake(function () { + return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; + }); + + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + wda.webDriverAgentUrl.should.equal('http://127.0.0.1:8100/'); + }); + + it('should call uninstall if current revision differs from the bundled one', async function () { + wdaStub.callsFake(function () { + return {build: { upgradedAt: '1' }}; + }); + getTimestampStub.callsFake(() => '2'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.calledOnce.should.be.true; + }); + + it('should not call uninstall if current revision is the same as the bundled one', async function () { + wdaStub.callsFake(function () { + return {build: { upgradedAt: '1' }}; + }); + getTimestampStub.callsFake(() => '1'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + }); + + it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { + wdaStub.callsFake(function () { + return {build: {}}; + }); + getTimestampStub.callsFake(() => '1'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + }); + + it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { + wdaStub.callsFake(function () { + return {build: { upgradedAt: '1' }}; + }); + getTimestampStub.callsFake(() => null); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + wdaStub.calledOnce.should.be.true; + wdaStubUninstall.notCalled.should.be.true; + }); + + describe('uninstall', function () { + let device; + let wda; + let deviceGetBundleIdsStub; + let deviceRemoveAppStub; + + beforeEach(function () { + device = { + getUserInstalledBundleIdsByBundleName: () => {}, + removeApp: () => {} + }; + wda = new WebDriverAgent('1', {device}); + deviceGetBundleIdsStub = sinon.stub(device, 'getUserInstalledBundleIdsByBundleName'); + deviceRemoveAppStub = sinon.stub(device, 'removeApp'); + }); + + afterEach(function () { + for (const stub of [deviceGetBundleIdsStub, deviceRemoveAppStub]) { + if (stub) { + stub.reset(); + } + } + }); + + it('should not call uninstall', async function () { + deviceGetBundleIdsStub.callsFake(() => []); + + await wda.uninstall(); + deviceGetBundleIdsStub.calledOnce.should.be.true; + deviceRemoveAppStub.notCalled.should.be.true; + }); + + it('should call uninstall once', async function () { + const uninstalledBundIds = []; + deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1']); + deviceRemoveAppStub.callsFake((id) => uninstalledBundIds.push(id)); + + await wda.uninstall(); + deviceGetBundleIdsStub.calledOnce.should.be.true; + deviceRemoveAppStub.calledOnce.should.be.true; + uninstalledBundIds.should.eql(['com.appium.WDA1']); + }); + + it('should call uninstall twice', async function () { + const uninstalledBundIds = []; + deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1', 'com.appium.WDA2']); + deviceRemoveAppStub.callsFake((id) => uninstalledBundIds.push(id)); + + await wda.uninstall(); + deviceGetBundleIdsStub.calledOnce.should.be.true; + deviceRemoveAppStub.calledTwice.should.be.true; + uninstalledBundIds.should.eql(['com.appium.WDA1', 'com.appium.WDA2']); + }); + }); +}); + + +describe('usePreinstalledWDA related functions', function () { + describe('bundleIdForXctest', function () { + it('should have xctrunner automatically', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + const agent = new WebDriverAgent({}, args); + agent.bundleIdForXctest.should.equal('io.appium.wda.xctrunner'); + }); + + it('should have xctrunner automatically with default bundle id', function () { + const args = Object.assign({}, fakeConstructorArgs); + const agent = new WebDriverAgent({}, args); + agent.bundleIdForXctest.should.equal('com.facebook.WebDriverAgentRunner.xctrunner'); + }); + + it('should allow an empty string as xctrunner suffix', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + args.updatedWDABundleIdSuffix = ''; + const agent = new WebDriverAgent({}, args); + agent.bundleIdForXctest.should.equal('io.appium.wda'); + }); + + it('should allow an empty string as xctrunner suffix with default bundle id', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleIdSuffix = ''; + const agent = new WebDriverAgent({}, args); + agent.bundleIdForXctest.should.equal('com.facebook.WebDriverAgentRunner'); + }); + + it('should have an arbitrary xctrunner suffix', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + args.updatedWDABundleIdSuffix = '.customsuffix'; + const agent = new WebDriverAgent({}, args); + agent.bundleIdForXctest.should.equal('io.appium.wda.customsuffix'); + }); + + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e7becfe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@appium/tsconfig/tsconfig.json", + "compilerOptions": { + "strict": false, // TODO: make this flag true + "esModuleInterop": true, + "outDir": "build", + "types": ["node"], + "checkJs": true + }, + "include": [ + "index.ts", + "lib" + ] +}