Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions UITests/KeyboardResizeUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ final class KeyboardResizeUITests: XCTestCase {
let app = XCUIApplication()
app.launchArguments = ["--ui-testing"]
app.launch()
app.buttons["addHost"].tapWhenReady()
app.textFields["hostName"].typeWhenReady("Keyboard resize")
app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
app.textFields["hostUsername"].typeWhenReady("tester")
app.buttons["saveHost"].tapWhenReady()
app.buttons["host-Keyboard resize"].tapWhenReady()
try app.buttons["addHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Keyboard resize")
try app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
try app.textFields["hostUsername"].typeWhenReady("tester")
try app.buttons["saveHost"].tapWhenReady()
try app.buttons["host-Keyboard resize"].tapWhenReady()
XCTAssertTrue(app.secureTextFields["Password"].waitForExistence(timeout: 10))
// #35 landed here. A tap delivered while the credential sheet was still animating in
// was dropped — worse on an iPad, where it is a form sheet that travels further and
// settles later — and the sheet then stayed up with the connection still `.connecting`,
// so the button waited for below, which `TerminalScreen` only draws once the phase
// leaves `.connecting`, never appeared at all. `tapWhenReady` is what stops that.
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
// Through `waitUntil` rather than `waitForExistence` so a failure here keeps a
// screenshot: it is what would say whether the sheet is still up or the phase never
// moved. Cancelling resumes the credential continuation with nil and the connection
Expand Down
6 changes: 4 additions & 2 deletions UITests/TailscaleImportUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ final class TailscaleImportUITests: XCTestCase {

let settings = UIDevice.current.userInterfaceIdiom == .pad
? app.buttons["workspaceSettings"] : app.buttons["Settings"]
settings.tapWhenReady()
try settings.tapWhenReady()
XCTAssertTrue(app.staticTexts["Terminal font preview"].waitForExistence(timeout: 5))
app.navigationBars["Settings"].buttons["Done"].tap()
XCTAssertTrue(app.buttons["importTailscaleHosts"].waitForExistence(timeout: 5))
Expand Down Expand Up @@ -179,7 +179,9 @@ final class TailscaleImportUITests: XCTestCase {
let deadline = ContinuousClock.now.advanced(by: .seconds(3))
while ContinuousClock.now < deadline {
if element.isHittable { return }
_ = element.waitForExistence(timeout: 0.2)
// Sleeping rather than `waitForExistence`, which returns instantly once the
// element exists and so throttles nothing. See `waitUntilHittable`.
Thread.sleep(forTimeInterval: 0.05)
}
app.swipeUp()
}
Expand Down
32 changes: 27 additions & 5 deletions UITests/UITestInteraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ extension XCUIElement {
/// The timeout is patience, not a deadline anything has to meet: reaching it means the test
/// was going to fail regardless, and twenty seconds is not long on a CI runner that has been
/// building for half an hour and is on its second simulator.
///
/// Throws rather than only recording a failure. `continueAfterFailure` is true by default, so
/// a helper that failed and returned would let the caller carry on: `typeWhenReady` would
/// then type into nothing and raise a second, unrelated "no keyboard focus" error on top of
/// the real one, and every later helper in the test would spend its own twenty seconds
/// before doing the same — three times over, now that CI retries. Stopping at the first
/// unmet precondition keeps the report about the thing that actually went wrong.
@MainActor
func tapWhenReady(timeout: TimeInterval = 20, file: StaticString = #filePath, line: UInt = #line) {
func tapWhenReady(timeout: TimeInterval = 20, file: StaticString = #filePath, line: UInt = #line) throws {
guard waitUntilHittable(timeout: timeout) else {
XCTFail("\(self) never became hittable", file: file, line: line)
return
throw UITestInteractionError.neverBecameHittable(String(describing: self))
}
tap()
}

/// Taps and types in one step, which is the shape every form in these tests uses.
@MainActor
func typeWhenReady(_ text: String, timeout: TimeInterval = 20,
file: StaticString = #filePath, line: UInt = #line) {
tapWhenReady(timeout: timeout, file: file, line: line)
file: StaticString = #filePath, line: UInt = #line) throws {
try tapWhenReady(timeout: timeout, file: file, line: line)
typeText(text)
}

Expand All @@ -43,8 +50,23 @@ extension XCUIElement {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if exists, isHittable { return true }
_ = waitForExistence(timeout: 0.1)
// An explicit sleep, not `waitForExistence(timeout:)`. That returns immediately when
// the element already exists, which is exactly the case here — present but not yet
// touchable — so using it as the throttle spins on back-to-back accessibility
// snapshots for the whole timeout. Querying the app that hard is a poor way to wait
// for it to settle.
Thread.sleep(forTimeInterval: 0.05)
}
return exists && isHittable
}
}

enum UITestInteractionError: Error, CustomStringConvertible {
case neverBecameHittable(String)

var description: String {
switch self {
case let .neverBecameHittable(element): return "\(element) never became hittable"
}
}
}
60 changes: 30 additions & 30 deletions UITests/iOSSHUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ final class iOSSHUITests: XCTestCase {
let app = XCUIApplication()
app.launchArguments = ["--ui-testing"]
app.launch()
app.buttons["addFirstHost"].tapWhenReady()
app.textFields["hostName"].typeWhenReady("Japanese input")
app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
app.textFields["hostUsername"].typeWhenReady("tester")
app.buttons["saveHost"].tapWhenReady()
app.buttons["host-Japanese input"].tapWhenReady()
try app.buttons["addFirstHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Japanese input")
try app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
try app.textFields["hostUsername"].typeWhenReady("tester")
try app.buttons["saveHost"].tapWhenReady()
try app.buttons["host-Japanese input"].tapWhenReady()
XCTAssertTrue(app.secureTextFields["Password"].waitForExistence(timeout: 10))
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
XCTAssertTrue(app.buttons["Reconnect"].waitForExistence(timeout: 5))
let terminal = app.descendants(matching: .any).matching(identifier: "terminal").firstMatch
terminal.tap()
Expand Down Expand Up @@ -70,14 +70,14 @@ final class iOSSHUITests: XCTestCase {
let app = XCUIApplication()
app.launchArguments = ["--ui-testing"]
app.launch()
app.buttons["addFirstHost"].tapWhenReady()
app.textFields["hostName"].typeWhenReady("Viewport")
app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
app.textFields["hostUsername"].typeWhenReady("tester")
app.buttons["saveHost"].tapWhenReady()
try app.buttons["addFirstHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Viewport")
try app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
try app.textFields["hostUsername"].typeWhenReady("tester")
try app.buttons["saveHost"].tapWhenReady()
app.buttons["host-Viewport"].tap()
XCTAssertTrue(app.secureTextFields["Password"].waitForExistence(timeout: 10))
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
XCTAssertTrue(app.buttons["Reconnect"].waitForExistence(timeout: 5))
let terminal = app.descendants(matching: .any).matching(identifier: "terminal").firstMatch
terminal.tap()
Expand All @@ -90,7 +90,7 @@ final class iOSSHUITests: XCTestCase {

app.buttons["Reconnect"].tap()
XCTAssertTrue(app.secureTextFields["Password"].waitForExistence(timeout: 10))
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
terminal.tap()
try await assertTerminalIsAboveAccessory(app)

Expand Down Expand Up @@ -130,14 +130,14 @@ final class iOSSHUITests: XCTestCase {
let app = XCUIApplication()
app.launchArguments = ["--ui-testing"]
app.launch()
app.buttons["addFirstHost"].tapWhenReady()
app.textFields["hostName"].typeWhenReady("Terminal test")
app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
app.textFields["hostUsername"].typeWhenReady("tester")
app.buttons["saveHost"].tapWhenReady()
try app.buttons["addFirstHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Terminal test")
try app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
try app.textFields["hostUsername"].typeWhenReady("tester")
try app.buttons["saveHost"].tapWhenReady()
app.buttons["host-Terminal test"].tap()
XCTAssertTrue(app.secureTextFields["Password"].waitForExistence(timeout: 10))
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
XCTAssertTrue(app.buttons["Reconnect"].waitForExistence(timeout: 5))
XCTAssertTrue(app.descendants(matching: .any).matching(identifier: "terminal").firstMatch.exists)
XCTAssertEqual(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "rendering could not start")).count, 0)
Expand All @@ -155,19 +155,19 @@ final class iOSSHUITests: XCTestCase {
app.launchArguments = ["--ui-testing"]
app.launch()
XCTAssertTrue(app.buttons["addFirstHost"].waitForExistence(timeout: 10))
app.buttons["addFirstHost"].tapWhenReady()
try app.buttons["addFirstHost"].tapWhenReady()
XCTAssertFalse(app.buttons["saveHost"].isEnabled)
app.textFields["hostName"].typeWhenReady("Development")
app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
app.textFields["hostUsername"].typeWhenReady("developer")
app.buttons["saveHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Development")
try app.textFields["hostAddress"].typeWhenReady("192.0.2.1")
try app.textFields["hostUsername"].typeWhenReady("developer")
try app.buttons["saveHost"].tapWhenReady()
let host = app.buttons["host-Development"]
XCTAssertTrue(host.waitForExistence(timeout: 5))
host.swipeRight()
app.buttons["Edit"].tap()
XCTAssertEqual(app.textFields["hostAddress"].value as? String, "192.0.2.1")
XCTAssertEqual(app.textFields["hostUsername"].value as? String, "developer")
app.buttons["Cancel"].tapWhenReady()
try app.buttons["Cancel"].tapWhenReady()
host.swipeLeft()
app.buttons["Delete"].tap()
XCTAssertTrue(app.buttons["addFirstHost"].waitForExistence(timeout: 5))
Expand All @@ -187,10 +187,10 @@ final class iOSSHUITests: XCTestCase {
XCTAssertTrue(privacyPolicyLink.waitForExistence(timeout: 5))
XCTAssertTrue(app.descendants(matching: .any)["supportLink"].waitForExistence(timeout: 5))
app.buttons["Done"].tap()
app.buttons["addFirstHost"].tapWhenReady()
app.textFields["hostName"].typeWhenReady("Bad host")
app.textFields["hostAddress"].typeWhenReady("bad host")
app.textFields["hostUsername"].typeWhenReady("root")
try app.buttons["addFirstHost"].tapWhenReady()
try app.textFields["hostName"].typeWhenReady("Bad host")
try app.textFields["hostAddress"].typeWhenReady("bad host")
try app.textFields["hostUsername"].typeWhenReady("root")
XCTAssertFalse(app.buttons["saveHost"].isEnabled)
}
}
Loading