appium-webdriveragent 16.2.2 → 16.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h +3 -3
- package/PrivateHeaders/XCTest/XCUIApplication.h +1 -1
- package/PrivateHeaders/XCTest/XCUIDevice.h +0 -1
- package/PrivateHeaders/XCTest/XCUIElement.h +1 -1
- package/Scripts/build.sh +6 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +1482 -2
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_watchOS.xcscheme +76 -0
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme +86 -0
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentLib_watchOS.xcscheme +85 -0
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme +1 -1
- package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_watchOS.xcscheme +128 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +5 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +2 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +1 -1
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +34 -0
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +76 -4
- package/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h +1 -1
- package/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +1 -1
- package/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m +1 -1
- package/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +11 -0
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +55 -4
- package/WebDriverAgentLib/Commands/FBElementCommands.m +6 -5
- package/WebDriverAgentLib/Commands/FBOrientationCommands.m +1 -1
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +29 -5
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBTCPSocket.h +70 -0
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +199 -0
- package/WebDriverAgentLib/Routing/FBWebServer.m +33 -3
- package/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.h +68 -0
- package/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m +376 -0
- package/WebDriverAgentLib/Routing/WatchOS/RouteRequest.h +30 -0
- package/WebDriverAgentLib/Routing/WatchOS/RouteRequest.m +25 -0
- package/WebDriverAgentLib/Routing/WatchOS/RouteResponse.h +30 -0
- package/WebDriverAgentLib/Routing/WatchOS/RouteResponse.m +54 -0
- package/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +8 -0
- package/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h +1 -1
- package/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m +1 -1
- package/WebDriverAgentLib/Utilities/FBConfiguration.h +1 -1
- package/WebDriverAgentLib/Utilities/FBConfiguration.m +3 -3
- package/WebDriverAgentLib/Utilities/FBImageProcessor.m +7 -2
- package/WebDriverAgentLib/Utilities/FBMacros.h +14 -5
- package/WebDriverAgentLib/Utilities/FBMathUtils.h +1 -1
- package/WebDriverAgentLib/Utilities/FBMathUtils.m +1 -1
- package/WebDriverAgentLib/Utilities/FBPasteboard.h +1 -1
- package/WebDriverAgentLib/Utilities/FBPasteboard.m +1 -1
- package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +2 -2
- package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h +1 -1
- package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +1 -1
- package/WebDriverAgentTests/IntegrationApp_watchOS/ContentView.swift +31 -0
- package/WebDriverAgentTests/IntegrationApp_watchOS/WatchSpikeApp.swift +18 -0
- package/WebDriverAgentTests/IntegrationTests/FBForceTouchTests.m +3 -0
- package/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m +3 -2
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAlertIntegrationTests.swift +23 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAppLifecycleIntegrationTests.swift +57 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAClickIntegrationTests.swift +31 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDADeviceIntegrationTests.swift +113 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAElementAttributeIntegrationTests.swift +57 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAFindIntegrationTests.swift +126 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAScreenshotAndSourceIntegrationTests.swift +52 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDASessionIntegrationTests.swift +72 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDATypingIntegrationTests.swift +57 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAUnknownCommandIntegrationTests.swift +24 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchHTTPClient.swift +95 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchIntegrationTestCase.swift +61 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Covers FBFindElementCommands: find/sub-find/active-element, not-found errors, and every
|
|
12
|
+
/// locator strategy (accessibility id/name/id, class name, class chain, xpath, predicate
|
|
13
|
+
/// string, link text). getVisibleCells is skipped - no table/collection view to test it against.
|
|
14
|
+
final class WDAFindIntegrationTests: WDAWatchIntegrationTestCase {
|
|
15
|
+
func testFindElementByAccessibilityId() throws {
|
|
16
|
+
let elementId = try findElement(byAccessibilityId: "tapMeButton")
|
|
17
|
+
XCTAssertFalse(elementId.isEmpty)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testFindElementByNameAndIdAliases() throws {
|
|
21
|
+
// "name" and "id" are aliases for "accessibility id".
|
|
22
|
+
for using in ["name", "id"] {
|
|
23
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
24
|
+
"using": using,
|
|
25
|
+
"value": "tapMeButton",
|
|
26
|
+
])
|
|
27
|
+
XCTAssertEqual(response.statusCode, 200, "using: \(using)")
|
|
28
|
+
XCTAssertNotNil(response.valueDict?["ELEMENT"], "using: \(using)")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testFindElementByPredicateString() throws {
|
|
33
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
34
|
+
"using": "predicate string",
|
|
35
|
+
"value": "label == \"Tap Me\"",
|
|
36
|
+
])
|
|
37
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
38
|
+
XCTAssertNotNil(response.valueDict?["ELEMENT"])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testFindElementsByClassChain() throws {
|
|
42
|
+
let response = try client.post("/session/\(sessionId!)/elements", body: [
|
|
43
|
+
"using": "class chain",
|
|
44
|
+
"value": "**/XCUIElementTypeButton",
|
|
45
|
+
])
|
|
46
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
47
|
+
XCTAssertGreaterThanOrEqual((response.value as? [[String: Any]])?.count ?? 0, 1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func testFindElementByXPath() throws {
|
|
51
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
52
|
+
"using": "xpath",
|
|
53
|
+
"value": "//XCUIElementTypeButton[@name=\"tapMeButton\"]",
|
|
54
|
+
])
|
|
55
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
56
|
+
XCTAssertNotNil(response.valueDict?["ELEMENT"])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func testFindElementByLinkTextAndPartialLinkText() throws {
|
|
60
|
+
// WDA matches (partial) link text against `name`, not an actual hyperlink search.
|
|
61
|
+
for using in ["link text", "partial link text"] {
|
|
62
|
+
let value = using == "link text" ? "tapMeButton" : "tapMe"
|
|
63
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
64
|
+
"using": using,
|
|
65
|
+
"value": value,
|
|
66
|
+
])
|
|
67
|
+
XCTAssertEqual(response.statusCode, 200, "using: \(using)")
|
|
68
|
+
XCTAssertNotNil(response.valueDict?["ELEMENT"], "using: \(using)")
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testFindElementThatDoesNotExistReturnsAnError() throws {
|
|
73
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
74
|
+
"using": "accessibility id",
|
|
75
|
+
"value": "thisElementDoesNotExist",
|
|
76
|
+
])
|
|
77
|
+
XCTAssertNotEqual(response.statusCode, 200)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testFindElements() throws {
|
|
81
|
+
let response = try client.post("/session/\(sessionId!)/elements", body: [
|
|
82
|
+
"using": "class name",
|
|
83
|
+
"value": "XCUIElementTypeButton",
|
|
84
|
+
])
|
|
85
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
86
|
+
guard let elements = response.value as? [[String: Any]] else {
|
|
87
|
+
return XCTFail("Expected an array of elements: \(String(describing: response.json))")
|
|
88
|
+
}
|
|
89
|
+
XCTAssertGreaterThanOrEqual(elements.count, 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func testFindSubElementAndSubElements() throws {
|
|
93
|
+
let windowResponse = try client.post("/session/\(sessionId!)/element", body: [
|
|
94
|
+
"using": "class name",
|
|
95
|
+
"value": "XCUIElementTypeWindow",
|
|
96
|
+
])
|
|
97
|
+
guard let windowId = windowResponse.valueDict?["ELEMENT"] as? String else {
|
|
98
|
+
return XCTFail("Could not find the root window element")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let subElementResponse = try client.post("/session/\(sessionId!)/element/\(windowId)/element", body: [
|
|
102
|
+
"using": "accessibility id",
|
|
103
|
+
"value": "tapMeButton",
|
|
104
|
+
])
|
|
105
|
+
XCTAssertEqual(subElementResponse.statusCode, 200)
|
|
106
|
+
XCTAssertNotNil(subElementResponse.valueDict?["ELEMENT"])
|
|
107
|
+
|
|
108
|
+
let subElementsResponse = try client.post("/session/\(sessionId!)/element/\(windowId)/elements", body: [
|
|
109
|
+
"using": "class name",
|
|
110
|
+
"value": "XCUIElementTypeButton",
|
|
111
|
+
])
|
|
112
|
+
XCTAssertEqual(subElementsResponse.statusCode, 200)
|
|
113
|
+
XCTAssertGreaterThanOrEqual((subElementsResponse.value as? [[String: Any]])?.count ?? 0, 1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func testActiveElementResolvesToTheFocusedTextField() throws {
|
|
117
|
+
let fieldId = try findElement(byAccessibilityId: "typingField")
|
|
118
|
+
try client.post("/session/\(sessionId!)/element/\(fieldId)/click")
|
|
119
|
+
// Give the keyboard-sheet transition a moment to finish before checking focus.
|
|
120
|
+
Thread.sleep(forTimeInterval: 1)
|
|
121
|
+
|
|
122
|
+
let response = try client.get("/session/\(sessionId!)/element/active")
|
|
123
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
124
|
+
XCTAssertNotNil(response.valueDict?["ELEMENT"])
|
|
125
|
+
}
|
|
126
|
+
}
|
package/WebDriverAgentTests/IntegrationTests_watchOS/WDAScreenshotAndSourceIntegrationTests.swift
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Covers device/element screenshots and /source, /wda/accessibleSource.
|
|
12
|
+
final class WDAScreenshotAndSourceIntegrationTests: WDAWatchIntegrationTestCase {
|
|
13
|
+
private static let pngMagicBytes: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
|
|
14
|
+
|
|
15
|
+
private func assertLooksLikeAPNG(_ base64String: String?, file: StaticString = #filePath, line: UInt = #line) {
|
|
16
|
+
guard let base64String = base64String, let data = Data(base64Encoded: base64String) else {
|
|
17
|
+
return XCTFail("Response value was not base64 data", file: file, line: line)
|
|
18
|
+
}
|
|
19
|
+
XCTAssertGreaterThan(data.count, Self.pngMagicBytes.count, file: file, line: line)
|
|
20
|
+
XCTAssertEqual(Array(data.prefix(Self.pngMagicBytes.count)), Self.pngMagicBytes, "response was not a PNG", file: file, line: line)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func testDeviceScreenshot() throws {
|
|
24
|
+
let response = try client.get("/session/\(sessionId!)/screenshot")
|
|
25
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
26
|
+
assertLooksLikeAPNG(response.valueString)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testElementScreenshot() throws {
|
|
30
|
+
let buttonId = try findElement(byAccessibilityId: "tapMeButton")
|
|
31
|
+
let response = try client.get("/session/\(sessionId!)/element/\(buttonId)/screenshot")
|
|
32
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
33
|
+
assertLooksLikeAPNG(response.valueString)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testPageSourceContainsTheKnownElements() throws {
|
|
37
|
+
let response = try client.get("/session/\(sessionId!)/source?format=xml")
|
|
38
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
39
|
+
guard let xml = response.valueString else {
|
|
40
|
+
return XCTFail("No XML source returned")
|
|
41
|
+
}
|
|
42
|
+
XCTAssertTrue(xml.contains("tapMeButton"))
|
|
43
|
+
XCTAssertTrue(xml.contains("resultLabel"))
|
|
44
|
+
XCTAssertTrue(xml.contains("typingField"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testAccessibleSource() throws {
|
|
48
|
+
let response = try client.get("/session/\(sessionId!)/wda/accessibleSource")
|
|
49
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
50
|
+
XCTAssertNotNil(response.value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Covers FBSessionCommands. Manages its own session lifecycle per test, since that's what's
|
|
12
|
+
/// under test here, rather than using WDAWatchIntegrationTestCase.
|
|
13
|
+
final class WDASessionIntegrationTests: XCTestCase {
|
|
14
|
+
let client = WDAWatchHTTPClient()
|
|
15
|
+
|
|
16
|
+
override func setUp() {
|
|
17
|
+
super.setUp()
|
|
18
|
+
continueAfterFailure = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func testStatusIsReadyWithoutASession() throws {
|
|
22
|
+
let response = try client.get("/status")
|
|
23
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
24
|
+
XCTAssertEqual(response.valueDict?["ready"] as? Bool, true)
|
|
25
|
+
XCTAssertEqual(response.valueDict?["state"] as? String, "success")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testHealthCheck() throws {
|
|
29
|
+
let response = try client.get("/wda/healthcheck")
|
|
30
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testCreateGetActiveAndDeleteSessionLifecycle() throws {
|
|
34
|
+
let createResponse = try client.post("/session", body: [
|
|
35
|
+
"capabilities": ["alwaysMatch": ["bundleId": WDAWatchIntegrationTestCase.integrationAppBundleId]]
|
|
36
|
+
])
|
|
37
|
+
XCTAssertEqual(createResponse.statusCode, 200)
|
|
38
|
+
guard let sessionId = createResponse.valueDict?["sessionId"] as? String else {
|
|
39
|
+
return XCTFail("No sessionId in create-session response: \(String(describing: createResponse.json))")
|
|
40
|
+
}
|
|
41
|
+
XCTAssertFalse(sessionId.isEmpty)
|
|
42
|
+
|
|
43
|
+
let activeResponse = try client.get("/session/\(sessionId)")
|
|
44
|
+
XCTAssertEqual(activeResponse.statusCode, 200)
|
|
45
|
+
XCTAssertEqual(activeResponse.valueDict?["sessionId"] as? String, sessionId)
|
|
46
|
+
|
|
47
|
+
let deleteResponse = try client.delete("/session/\(sessionId)")
|
|
48
|
+
XCTAssertEqual(deleteResponse.statusCode, 200)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func testTimeoutsAndAppiumSettingsRoundTrip() throws {
|
|
52
|
+
let createResponse = try client.post("/session", body: [
|
|
53
|
+
"capabilities": ["alwaysMatch": ["bundleId": WDAWatchIntegrationTestCase.integrationAppBundleId]]
|
|
54
|
+
])
|
|
55
|
+
guard let sessionId = createResponse.valueDict?["sessionId"] as? String else {
|
|
56
|
+
return XCTFail("No sessionId in create-session response")
|
|
57
|
+
}
|
|
58
|
+
defer { _ = try? client.delete("/session/\(sessionId)") }
|
|
59
|
+
|
|
60
|
+
let timeoutsResponse = try client.post("/session/\(sessionId)/timeouts", body: ["implicit": 500])
|
|
61
|
+
XCTAssertEqual(timeoutsResponse.statusCode, 200)
|
|
62
|
+
|
|
63
|
+
let getSettingsResponse = try client.get("/session/\(sessionId)/appium/settings")
|
|
64
|
+
XCTAssertEqual(getSettingsResponse.statusCode, 200)
|
|
65
|
+
XCTAssertNotNil(getSettingsResponse.valueDict)
|
|
66
|
+
|
|
67
|
+
let setSettingsResponse = try client.post("/session/\(sessionId)/appium/settings", body: [
|
|
68
|
+
"settings": ["shouldUseCompactResponses": false]
|
|
69
|
+
])
|
|
70
|
+
XCTAssertEqual(setSettingsResponse.statusCode, 200)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Covers /element/:uuid/value, /element/:uuid/clear, and /wda/keyboard/dismiss.
|
|
12
|
+
///
|
|
13
|
+
/// KNOWN LIMITATION (see XCUIElement+FBTyping.m): the keyboard opens and the field gets focus,
|
|
14
|
+
/// but no keystrokes land. Wrapped in XCTExpectFailure so this re-verifies itself and starts
|
|
15
|
+
/// failing loudly the moment it starts working, instead of silently staying stale.
|
|
16
|
+
final class WDATypingIntegrationTests: WDAWatchIntegrationTestCase {
|
|
17
|
+
func testSetValueOpensTheKeyboardAndAttemptsToTypeIntoTheField() throws {
|
|
18
|
+
let fieldId = try findElement(byAccessibilityId: "typingField")
|
|
19
|
+
XCTAssertEqual(try attributeValue(fieldId, "value"), "Type here", "expected the untouched placeholder")
|
|
20
|
+
|
|
21
|
+
let setValueResponse = try client.post("/session/\(sessionId!)/element/\(fieldId)/value", body: [
|
|
22
|
+
"value": ["h", "i"]
|
|
23
|
+
])
|
|
24
|
+
XCTAssertEqual(setValueResponse.statusCode, 200, "the route itself should not error even though typing doesn't land")
|
|
25
|
+
|
|
26
|
+
XCTExpectFailure("watchOS keystroke delivery into the on-screen keyboard is a known unresolved limitation") {
|
|
27
|
+
let value = try? attributeValue(fieldId, "value")
|
|
28
|
+
XCTAssertEqual(value, "hi")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Back out so later tests start clean.
|
|
32
|
+
if let cancelId = try? findElement(byAccessibilityId: "Cancel") {
|
|
33
|
+
_ = try? client.post("/session/\(sessionId!)/element/\(cancelId)/click")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testClearingAnAlreadyEmptyFieldSucceeds() throws {
|
|
38
|
+
// Clearing an empty field short-circuits, so this works despite the typing limitation.
|
|
39
|
+
let fieldId = try findElement(byAccessibilityId: "typingField")
|
|
40
|
+
let clearResponse = try client.post("/session/\(sessionId!)/element/\(fieldId)/clear")
|
|
41
|
+
XCTAssertEqual(clearResponse.statusCode, 200)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testDismissKeyboardRouteRespondsWithoutCrashingTheServer() throws {
|
|
45
|
+
// Only asserts the route round-trips cleanly, not that dismissal actually happens.
|
|
46
|
+
let fieldId = try findElement(byAccessibilityId: "typingField")
|
|
47
|
+
try client.post("/session/\(sessionId!)/element/\(fieldId)/click")
|
|
48
|
+
Thread.sleep(forTimeInterval: 1)
|
|
49
|
+
|
|
50
|
+
_ = try client.post("/session/\(sessionId!)/wda/keyboard/dismiss")
|
|
51
|
+
|
|
52
|
+
// Back out regardless of whether dismiss actually closed the sheet.
|
|
53
|
+
if let cancelId = try? findElement(byAccessibilityId: "Cancel") {
|
|
54
|
+
_ = try? client.post("/session/\(sessionId!)/element/\(cancelId)/click")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Covers the catch-all 404 routes and the OPTIONS ping route - neither needs a session.
|
|
12
|
+
final class WDAUnknownCommandIntegrationTests: XCTestCase {
|
|
13
|
+
let client = WDAWatchHTTPClient()
|
|
14
|
+
|
|
15
|
+
func testUnknownRouteReturnsAnError() throws {
|
|
16
|
+
let response = try client.get("/this/route/does/not/exist")
|
|
17
|
+
XCTAssertNotEqual(response.statusCode, 200)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testPing() throws {
|
|
21
|
+
let response = try client.send("OPTIONS", "/anything")
|
|
22
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
/// Minimal synchronous JSON/HTTP client for driving the real WebDriverAgentRunner_watchOS
|
|
12
|
+
/// server, the same way a real client (Appium, curl) would. Requires the server to already be
|
|
13
|
+
/// running, e.g. via `xcodebuild test -scheme WebDriverAgentRunner_watchOS`.
|
|
14
|
+
struct WDAWatchHTTPClient {
|
|
15
|
+
struct Response {
|
|
16
|
+
let statusCode: Int
|
|
17
|
+
let json: [String: Any]?
|
|
18
|
+
|
|
19
|
+
var value: Any? { json?["value"] }
|
|
20
|
+
var valueDict: [String: Any]? { value as? [String: Any] }
|
|
21
|
+
var valueString: String? { value as? String }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
enum ClientError: Error, CustomStringConvertible {
|
|
25
|
+
case requestFailed(String)
|
|
26
|
+
|
|
27
|
+
var description: String {
|
|
28
|
+
switch self {
|
|
29
|
+
case .requestFailed(let message): return message
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let baseURL: URL
|
|
35
|
+
|
|
36
|
+
init() {
|
|
37
|
+
let host = ProcessInfo.processInfo.environment["WDA_TEST_HOST"] ?? "127.0.0.1"
|
|
38
|
+
let port = ProcessInfo.processInfo.environment["WDA_TEST_PORT"] ?? "8100"
|
|
39
|
+
self.baseURL = URL(string: "http://\(host):\(port)")!
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Splits path/query before resolving against baseURL - appendingPathComponent would
|
|
43
|
+
/// percent-encode a literal "?" instead of treating it as a query delimiter.
|
|
44
|
+
private func url(for path: String) -> URL {
|
|
45
|
+
let parts = path.split(separator: "?", maxSplits: 1)
|
|
46
|
+
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
|
47
|
+
components.path = components.path + parts[0]
|
|
48
|
+
if parts.count > 1 {
|
|
49
|
+
components.percentEncodedQuery = String(parts[1])
|
|
50
|
+
}
|
|
51
|
+
return components.url!
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@discardableResult
|
|
55
|
+
func send(_ method: String, _ path: String, body: [String: Any]? = nil, timeout: TimeInterval = 20) throws -> Response {
|
|
56
|
+
var request = URLRequest(url: url(for: path))
|
|
57
|
+
request.httpMethod = method
|
|
58
|
+
request.timeoutInterval = timeout
|
|
59
|
+
if let body = body {
|
|
60
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
61
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
65
|
+
var resultData: Data?
|
|
66
|
+
var resultResponse: URLResponse?
|
|
67
|
+
var resultError: Error?
|
|
68
|
+
|
|
69
|
+
URLSession.shared.dataTask(with: request) { data, response, error in
|
|
70
|
+
resultData = data
|
|
71
|
+
resultResponse = response
|
|
72
|
+
resultError = error
|
|
73
|
+
semaphore.signal()
|
|
74
|
+
}.resume()
|
|
75
|
+
|
|
76
|
+
if semaphore.wait(timeout: .now() + timeout + 5) == .timedOut {
|
|
77
|
+
throw ClientError.requestFailed("Request to \(method) \(path) timed out")
|
|
78
|
+
}
|
|
79
|
+
if let resultError = resultError {
|
|
80
|
+
throw ClientError.requestFailed("Request to \(method) \(path) failed: \(resultError)")
|
|
81
|
+
}
|
|
82
|
+
guard let httpResponse = resultResponse as? HTTPURLResponse else {
|
|
83
|
+
throw ClientError.requestFailed("Request to \(method) \(path) returned no HTTP response")
|
|
84
|
+
}
|
|
85
|
+
var json: [String: Any]?
|
|
86
|
+
if let resultData = resultData, !resultData.isEmpty {
|
|
87
|
+
json = try? JSONSerialization.jsonObject(with: resultData) as? [String: Any]
|
|
88
|
+
}
|
|
89
|
+
return Response(statusCode: httpResponse.statusCode, json: json)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func get(_ path: String) throws -> Response { try send("GET", path) }
|
|
93
|
+
func post(_ path: String, body: [String: Any] = [:]) throws -> Response { try send("POST", path, body: body) }
|
|
94
|
+
func delete(_ path: String) throws -> Response { try send("DELETE", path) }
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import XCTest
|
|
10
|
+
|
|
11
|
+
/// Creates a fresh WDA session against IntegrationApp_watchOS per test, so each starts clean.
|
|
12
|
+
class WDAWatchIntegrationTestCase: XCTestCase {
|
|
13
|
+
static let integrationAppBundleId = ProcessInfo.processInfo.environment["WDA_TEST_BUNDLE_ID"]
|
|
14
|
+
?? "com.facebook.wda.IntegrationApp.watchOS"
|
|
15
|
+
|
|
16
|
+
let client = WDAWatchHTTPClient()
|
|
17
|
+
var sessionId: String!
|
|
18
|
+
|
|
19
|
+
override func setUp() {
|
|
20
|
+
super.setUp()
|
|
21
|
+
continueAfterFailure = false
|
|
22
|
+
do {
|
|
23
|
+
let response = try client.post("/session", body: [
|
|
24
|
+
"capabilities": ["alwaysMatch": ["bundleId": Self.integrationAppBundleId]]
|
|
25
|
+
])
|
|
26
|
+
guard response.statusCode == 200, let sessionId = response.valueDict?["sessionId"] as? String else {
|
|
27
|
+
XCTFail("Failed to create session: \(String(describing: response.json))")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
self.sessionId = sessionId
|
|
31
|
+
} catch {
|
|
32
|
+
XCTFail("Failed to create session: \(error)")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override func tearDown() {
|
|
37
|
+
if let sessionId = sessionId {
|
|
38
|
+
_ = try? client.delete("/session/\(sessionId)")
|
|
39
|
+
}
|
|
40
|
+
super.tearDown()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Finds an element by accessibility id and returns its WDA element UUID.
|
|
44
|
+
@discardableResult
|
|
45
|
+
func findElement(byAccessibilityId accessibilityId: String, file: StaticString = #filePath, line: UInt = #line) throws -> String {
|
|
46
|
+
let response = try client.post("/session/\(sessionId!)/element", body: [
|
|
47
|
+
"using": "accessibility id",
|
|
48
|
+
"value": accessibilityId,
|
|
49
|
+
])
|
|
50
|
+
guard response.statusCode == 200, let elementId = response.valueDict?["ELEMENT"] as? String else {
|
|
51
|
+
XCTFail("Could not find element '\(accessibilityId)': \(String(describing: response.json))", file: file, line: line)
|
|
52
|
+
throw WDAWatchHTTPClient.ClientError.requestFailed("element not found")
|
|
53
|
+
}
|
|
54
|
+
return elementId
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func attributeValue(_ elementId: String, _ name: String) throws -> String? {
|
|
58
|
+
let response = try client.get("/session/\(sessionId!)/element/\(elementId)/attribute/\(name)")
|
|
59
|
+
return response.valueString
|
|
60
|
+
}
|
|
61
|
+
}
|