719 lines
32 KiB
Swift
719 lines
32 KiB
Swift
import XCTest
|
|
import Foundation
|
|
|
|
final class BrowserOmnibarSuggestionsUITests: XCTestCase {
|
|
private var dataPath = ""
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
continueAfterFailure = false
|
|
dataPath = "/tmp/cmux-ui-test-omnibar-suggestions-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
|
|
// Terminate any lingering app from a prior test so its debounced
|
|
// history-save doesn't overwrite the seeded browser_history.json.
|
|
let cleanup = XCUIApplication()
|
|
cleanup.terminate()
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
}
|
|
|
|
func testOmnibarSuggestionsAlignToPillAndCmdNP() {
|
|
seedBrowserHistoryForTest(seedEntries: [
|
|
SeedEntry(url: "https://example.com/", title: "Example Domain", visitCount: 12, typedCount: 4),
|
|
SeedEntry(url: "https://example.org/", title: "Example Organization", visitCount: 9, typedCount: 3),
|
|
SeedEntry(url: "https://go.dev/", title: "The Go Programming Language", visitCount: 6, typedCount: 1),
|
|
])
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
// Keep suggestions deterministic for the keyboard-nav assertions.
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
// Focus omnibar.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let pill = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarPill").firstMatch
|
|
XCTAssertTrue(pill.waitForExistence(timeout: 6.0))
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
|
|
// Type a query that matches the seeded URL.
|
|
XCTAssertTrue(
|
|
typeQueryAndWaitForSuggestions(app: app, omnibar: omnibar, query: "exam", timeout: 6.0),
|
|
"Expected omnibar suggestions to appear for 'exam'"
|
|
)
|
|
|
|
// SwiftUI's accessibility typing for ScrollView can vary; match by identifier regardless of element type.
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
let row0 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.0").firstMatch
|
|
XCTAssertTrue(row0.waitForExistence(timeout: 6.0))
|
|
|
|
// Frame checks (screen coordinates).
|
|
let pillFrame = pill.frame
|
|
let suggestionsFrame = suggestionsElement.frame
|
|
attachElementDebug(name: "omnibar-pill", element: pill)
|
|
attachElementDebug(name: "omnibar-suggestions", element: suggestionsElement)
|
|
|
|
XCTAssertGreaterThan(pillFrame.width, 50)
|
|
XCTAssertGreaterThan(suggestionsFrame.width, 50)
|
|
|
|
let xTolerance: CGFloat = 3.0
|
|
let wTolerance: CGFloat = 3.0
|
|
|
|
XCTAssertLessThanOrEqual(abs(pillFrame.minX - suggestionsFrame.minX), xTolerance,
|
|
"Expected suggestions minX to match omnibar minX.\nPill: \(pillFrame)\nSug: \(suggestionsFrame)")
|
|
XCTAssertLessThanOrEqual(abs(pillFrame.width - suggestionsFrame.width), wTolerance,
|
|
"Expected suggestions width to match omnibar width.\nPill: \(pillFrame)\nSug: \(suggestionsFrame)")
|
|
XCTAssertGreaterThanOrEqual(
|
|
suggestionsFrame.minY,
|
|
pillFrame.maxY - 1.0,
|
|
"Expected suggestions popup to render below (not behind) the omnibar.\nPill: \(pillFrame)\nSug: \(suggestionsFrame)"
|
|
)
|
|
|
|
// Row 0 should be the autocompletable example.com history entry.
|
|
// Verify Cmd+N moves to row 1, Cmd+P returns to row 0, then Enter navigates.
|
|
let row1 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.1").firstMatch
|
|
XCTAssertTrue(row1.waitForExistence(timeout: 6.0))
|
|
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(row1, timeout: 3.0),
|
|
"Expected Cmd+N to move selection to row 1. row1Value=\(String(describing: row1.value))"
|
|
)
|
|
|
|
app.typeKey("p", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(row0, timeout: 3.0),
|
|
"Expected Cmd+P to move selection back to row 0. row0Value=\(String(describing: row0.value))"
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
// After committing the autocompletion candidate, the omnibar should contain the URL.
|
|
// Note: example.com may redirect to example.org in some environments.
|
|
XCTAssertTrue(
|
|
waitForCondition(timeout: 8.0) {
|
|
self.containsExampleDomain((omnibar.value as? String) ?? "")
|
|
},
|
|
"Expected omnibar to navigate to example.com after keyboard nav + Enter. value=\(String(describing: omnibar.value))"
|
|
)
|
|
}
|
|
|
|
func testOmnibarEscapeAndClickOutsideBehaveLikeChrome() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
// Keep suggestions deterministic.
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
XCTAssertTrue(
|
|
focusOmnibarWithCmdL(app: app, omnibar: omnibar, timeout: 4.0),
|
|
"Expected Cmd+L to place keyboard focus in omnibar before typing"
|
|
)
|
|
|
|
// Focus omnibar and navigate to example.com via autocompletion (row 0).
|
|
omnibar.typeText("exam")
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
// Row 0 is the autocompletion candidate (example.com). Enter commits it.
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
// Note: example.com may redirect to example.org in some environments.
|
|
XCTAssertTrue(
|
|
waitForCondition(timeout: 8.0) {
|
|
self.containsExampleDomain((omnibar.value as? String) ?? "")
|
|
},
|
|
"Expected committed omnibar value to contain example.com or example.org. value=\(String(describing: omnibar.value))"
|
|
)
|
|
XCTAssertTrue(containsExampleDomain((omnibar.value as? String) ?? ""))
|
|
|
|
// Type a new query to open the popup, then Escape should revert to the current URL.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
omnibar.typeText("meaning")
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
let reverted = (omnibar.value as? String) ?? ""
|
|
XCTAssertTrue(containsExampleDomain(reverted), "Expected Escape to revert omnibar to current URL. value=\(reverted)")
|
|
XCTAssertFalse(suggestionsElement.waitForExistence(timeout: 0.5), "Expected Escape to close suggestions popup")
|
|
|
|
// Second Escape should blur to the web view: typing should not change the omnibar value.
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
let beforeTyping = (omnibar.value as? String) ?? ""
|
|
app.typeText("zzz")
|
|
let afterTyping = (omnibar.value as? String) ?? ""
|
|
XCTAssertEqual(afterTyping, beforeTyping, "Expected typing after 2nd Escape to not modify omnibar (blurred)")
|
|
|
|
// Click outside should also discard edits and blur.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
omnibar.typeText("foo")
|
|
|
|
let window = app.windows.firstMatch
|
|
XCTAssertTrue(window.waitForExistence(timeout: 6.0))
|
|
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)).click()
|
|
|
|
// Give SwiftUI focus a moment to settle.
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
|
|
|
let afterClick = (omnibar.value as? String) ?? ""
|
|
if !containsExampleDomain(afterClick) {
|
|
// VM UI automation can occasionally keep focus in the text field after a coordinate click.
|
|
// Fall back to Escape so we still validate post-click revert/blur behavior.
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
}
|
|
let recoveredAfterClick = (omnibar.value as? String) ?? ""
|
|
XCTAssertTrue(containsExampleDomain(recoveredAfterClick), "Expected click-outside path to discard edits. value=\(recoveredAfterClick)")
|
|
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
|
|
let beforeOutsideTyping = (omnibar.value as? String) ?? ""
|
|
app.typeText("bbb")
|
|
let afterOutsideTyping = (omnibar.value as? String) ?? ""
|
|
XCTAssertEqual(afterOutsideTyping, beforeOutsideTyping, "Expected typing after click-outside to not modify omnibar (blurred)")
|
|
}
|
|
|
|
func testOmnibarSuggestionsCmdNPWhenAddressBarFocused() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_REMOTE_SUGGESTIONS_JSON"] = #"["go tutorial","go json","go fmt"]"#
|
|
launchAndEnsureForeground(app)
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
XCTAssertTrue(
|
|
typeQueryAndWaitForSuggestions(app: app, omnibar: omnibar, query: "go", timeout: 6.0),
|
|
"Expected omnibar suggestions to appear for 'go'"
|
|
)
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
let row1 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.1").firstMatch
|
|
let row2 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.2").firstMatch
|
|
XCTAssertTrue(row1.waitForExistence(timeout: 6.0))
|
|
XCTAssertTrue(row2.waitForExistence(timeout: 6.0))
|
|
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(row1, timeout: 3.0),
|
|
"Expected Cmd+N to move selection to row 1. row1Value=\(String(describing: row1.value))"
|
|
)
|
|
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(row2, timeout: 3.0),
|
|
"Expected repeated Cmd+N to move selection to row 2. row2Value=\(String(describing: row2.value))"
|
|
)
|
|
|
|
app.typeKey("p", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(row1, timeout: 3.0),
|
|
"Expected Cmd+P to move selection back to row 1. row1Value=\(String(describing: row1.value))"
|
|
)
|
|
}
|
|
|
|
func testOmnibarShowsMultipleRowsWithoutClipping() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_REMOTE_SUGGESTIONS_JSON"] = #"["go tutorial","go json","go fmt"]"#
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
omnibar.typeText("go")
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
let row2 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.2").firstMatch
|
|
XCTAssertTrue(row2.waitForExistence(timeout: 6.0), "Expected at least 3 suggestion rows for 'go'")
|
|
let popupFrame = suggestionsElement.frame
|
|
let row2Frame = row2.frame
|
|
XCTAssertGreaterThan(row2Frame.height, 1, "Expected third row to have a non-zero visible height")
|
|
XCTAssertLessThanOrEqual(row2Frame.maxY, popupFrame.maxY + 1, "Expected third row to stay inside popup bounds")
|
|
}
|
|
|
|
func testCmdLRefocusAfterNavigationKeepsOmnibarEditable() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
|
|
// Start a real navigation, then re-focus the omnibar immediately.
|
|
omnibar.typeText("example.com")
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
// Wait for navigation to finish so we can verify focus is held through page load.
|
|
var loadObserved = false
|
|
loadObserved = waitForCondition(timeout: 8.0) {
|
|
((omnibar.value as? String) ?? "").lowercased().contains("example.com")
|
|
}
|
|
XCTAssertTrue(loadObserved, "Expected omnibar to reflect the navigated URL after load. value=\(omnibar.value)")
|
|
|
|
let valueAfterLoad = (omnibar.value as? String) ?? ""
|
|
omnibar.typeText("zx")
|
|
|
|
var valueCaptured = false
|
|
valueCaptured = waitForCondition(timeout: 5.0) {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
return value.contains("zx") && value != valueAfterLoad
|
|
}
|
|
XCTAssertTrue(valueCaptured, "Expected omnirbar to keep keyboard focus after Cmd+L when navigation is in-flight. value=\(String(describing: omnibar.value))")
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(
|
|
suggestionsElement.waitForExistence(timeout: 3.0),
|
|
"Expected omnibar suggestions to appear while focused after Cmd+L during navigation"
|
|
)
|
|
|
|
// Avoid leaving test in partially edited state.
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
app.typeKey("x", modifierFlags: [])
|
|
}
|
|
|
|
func testCmdLImmediateTypingReplacesExistingURLBuffer() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
|
|
// Navigate to a non-empty URL first so Cmd+L must replace existing text.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
omnibar.typeText("example.com")
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
let loaded = waitForCondition(timeout: 8.0) {
|
|
let value = ((omnibar.value as? String) ?? "").lowercased()
|
|
return self.containsExampleDomain(value)
|
|
}
|
|
XCTAssertTrue(loaded, "Expected baseline navigation to load before Cmd+L fast-typing check.")
|
|
|
|
// Reproduce user flow: Cmd+L then immediate typing without waiting.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
app.typeText("lo")
|
|
|
|
var observedValue = ""
|
|
let startsWithTypedPrefix = waitForCondition(timeout: 7.0) {
|
|
observedValue = ((omnibar.value as? String) ?? "").lowercased()
|
|
return observedValue.hasPrefix("lo")
|
|
}
|
|
XCTAssertTrue(
|
|
startsWithTypedPrefix,
|
|
"Expected immediate typing after Cmd+L to preserve typed prefix 'lo'. value=\(observedValue)"
|
|
)
|
|
}
|
|
|
|
func testOmnibarAutocompleteCandidateIsCommittedOnEnter() {
|
|
seedBrowserHistoryForTest(
|
|
seedEntries: [
|
|
SeedEntry(url: "https://news.ycombinator.com/", title: "News Y Combinator", visitCount: 12, typedCount: 1),
|
|
SeedEntry(url: "https://gmail.com/", title: "Gmail", visitCount: 10, typedCount: 2),
|
|
]
|
|
)
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
|
|
omnibar.typeText("gm")
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
let rows: [XCUIElement] = (0...4).map {
|
|
app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.\($0)").firstMatch
|
|
}
|
|
XCTAssertTrue(rows[0].waitForExistence(timeout: 4.0))
|
|
|
|
var gmailRowIndex: Int?
|
|
_ = waitForCondition(timeout: 4.0) {
|
|
for (index, row) in rows.enumerated() where row.exists {
|
|
let rowValue = (row.value as? String) ?? ""
|
|
if rowValue.localizedCaseInsensitiveContains("gmail") {
|
|
gmailRowIndex = index
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
guard let gmailRowIndex else {
|
|
let rowValues = rows.enumerated().compactMap { index, row -> String? in
|
|
guard row.exists else { return nil }
|
|
return "row\(index)=\((row.value as? String) ?? "<nil>")"
|
|
}.joined(separator: ", ")
|
|
XCTFail("Expected a Gmail suggestion row. rows=\(rowValues)")
|
|
return
|
|
}
|
|
|
|
if gmailRowIndex > 0 {
|
|
let gmailRow = rows[gmailRowIndex]
|
|
for _ in 0..<gmailRowIndex {
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
}
|
|
XCTAssertTrue(
|
|
waitForSuggestionRowToBeSelected(gmailRow, timeout: 3.0),
|
|
"Expected Cmd+N to select Gmail row \(gmailRowIndex). value=\(String(describing: gmailRow.value))"
|
|
)
|
|
}
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
let committedToGmail = waitForCondition(timeout: 8.0) {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
return value.localizedCaseInsensitiveContains("gmail.com")
|
|
}
|
|
XCTAssertTrue(committedToGmail, "Expected Enter to commit Gmail autocomplete target. value=\(String(describing: omnibar.value))")
|
|
}
|
|
|
|
func testOmnibarSingleRowPopupUsesMinimumHeight() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
let query = "zzzz-\(UUID().uuidString.prefix(8))"
|
|
omnibar.typeText(query)
|
|
|
|
let suggestionsElement = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
XCTAssertTrue(suggestionsElement.waitForExistence(timeout: 6.0))
|
|
|
|
let row0 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.0").firstMatch
|
|
let row1 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.1").firstMatch
|
|
XCTAssertTrue(row0.waitForExistence(timeout: 6.0))
|
|
XCTAssertFalse(row1.waitForExistence(timeout: 0.5), "Expected one-row popup for a unique query")
|
|
|
|
let expectedMinHeight: CGFloat = 30
|
|
let tolerance: CGFloat = 2
|
|
let popupHeight = suggestionsElement.frame.height
|
|
XCTAssertLessThanOrEqual(
|
|
abs(popupHeight - expectedMinHeight),
|
|
tolerance,
|
|
"Expected one-row popup to use min height without extra bottom gap. frame=\(suggestionsElement.frame)"
|
|
)
|
|
|
|
let popupFrame = suggestionsElement.frame
|
|
let rowFrame = row0.frame
|
|
let topInset = rowFrame.minY - popupFrame.minY
|
|
let bottomInset = popupFrame.maxY - rowFrame.maxY
|
|
XCTAssertLessThanOrEqual(
|
|
abs(topInset - bottomInset),
|
|
1.5,
|
|
"Expected one-row popup to have balanced top/bottom insets. popup=\(popupFrame) row=\(rowFrame)"
|
|
)
|
|
}
|
|
|
|
func testInlineAutocompleteBackspaceDeletesTypedPrefixCharacter() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
omnibar.typeText("exam")
|
|
|
|
let valueAfterTyping = (omnibar.value as? String) ?? ""
|
|
XCTAssertTrue(
|
|
valueAfterTyping.contains("example.com"),
|
|
"Expected inline completion to display a URL for typed prefix. value=\(valueAfterTyping)"
|
|
)
|
|
XCTAssertFalse(
|
|
valueAfterTyping.lowercased().hasPrefix("https://"),
|
|
"Expected inline completion display to avoid injecting an https:// prefix unless typed."
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
|
|
let valueAfterDeleteAndEscape = (omnibar.value as? String) ?? ""
|
|
XCTAssertEqual(
|
|
valueAfterDeleteAndEscape,
|
|
"exa",
|
|
"Expected Backspace with inline suffix selected to remove one typed prefix character."
|
|
)
|
|
}
|
|
|
|
func testCmdASelectAllDoesNotClearInlineCompletion() {
|
|
seedBrowserHistoryForTest()
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_DISABLE_REMOTE_SUGGESTIONS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
omnibar.typeText("exam")
|
|
|
|
let typedPrefix = "exam"
|
|
var valueBeforeCmdA = ""
|
|
let sawInlineCompletion = waitForCondition(timeout: 3.0) {
|
|
valueBeforeCmdA = (omnibar.value as? String) ?? ""
|
|
let normalized = valueBeforeCmdA.lowercased()
|
|
return normalized.hasPrefix(typedPrefix) && valueBeforeCmdA.utf16.count > typedPrefix.utf16.count
|
|
}
|
|
XCTAssertTrue(
|
|
sawInlineCompletion,
|
|
"Expected inline completion to extend typed prefix before Cmd+A. value=\(valueBeforeCmdA)"
|
|
)
|
|
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.25))
|
|
|
|
let afterCmdA = (omnibar.value as? String) ?? ""
|
|
XCTAssertTrue(
|
|
afterCmdA.lowercased().hasPrefix(typedPrefix) && afterCmdA.utf16.count > typedPrefix.utf16.count,
|
|
"Expected Cmd+A to preserve inline completion display instead of collapsing to typed prefix. before=\(valueBeforeCmdA) after=\(afterCmdA)"
|
|
)
|
|
}
|
|
|
|
private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
|
|
app.launch()
|
|
XCTAssertTrue(
|
|
ensureForegroundAfterLaunch(app, timeout: timeout),
|
|
"Expected app to launch in foreground. state=\(app.state.rawValue)"
|
|
)
|
|
}
|
|
|
|
private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
|
if app.wait(for: .runningForeground, timeout: timeout) {
|
|
return true
|
|
}
|
|
if app.state == .runningBackground {
|
|
app.activate()
|
|
return app.wait(for: .runningForeground, timeout: 6.0)
|
|
}
|
|
return false
|
|
}
|
|
|
|
private struct SeedEntry {
|
|
let url: String
|
|
let title: String
|
|
var visitCount: Int = 2
|
|
var typedCount: Int = 0
|
|
}
|
|
|
|
private func seedBrowserHistoryForTest(entries: [(String, String)]? = nil, seedEntries: [SeedEntry]? = nil) {
|
|
// Keep the test hermetic: write a deterministic history file in the app's support dir
|
|
// so the omnibar always has at least one local suggestion row.
|
|
let fileManager = FileManager.default
|
|
guard let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
|
XCTFail("Missing Application Support directory")
|
|
return
|
|
}
|
|
|
|
let bundleId = "com.cmuxterm.app.debug"
|
|
let dir = appSupport.appendingPathComponent(bundleId, isDirectory: true)
|
|
let url = dir.appendingPathComponent("browser_history.json", isDirectory: false)
|
|
do {
|
|
try fileManager.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
} catch {
|
|
XCTFail("Failed to create app support dir: \(error)")
|
|
return
|
|
}
|
|
|
|
let now = Date().timeIntervalSinceReferenceDate
|
|
let resolved: [SeedEntry]
|
|
if let seedEntries {
|
|
resolved = seedEntries
|
|
} else if let entries {
|
|
resolved = entries.map { SeedEntry(url: $0.0, title: $0.1, visitCount: 10, typedCount: 2) }
|
|
} else {
|
|
resolved = [
|
|
SeedEntry(url: "https://example.com/", title: "Example Domain", visitCount: 10, typedCount: 2),
|
|
SeedEntry(url: "https://go.dev/", title: "The Go Programming Language", visitCount: 10, typedCount: 2),
|
|
SeedEntry(url: "https://www.google.com/", title: "Google", visitCount: 10, typedCount: 2),
|
|
]
|
|
}
|
|
let entriesJSON = resolved.enumerated().reversed().map { index, entry in
|
|
let recencyOffset = index * 120
|
|
var json = """
|
|
{
|
|
"id": "\(UUID().uuidString)",
|
|
"url": "\(entry.url)",
|
|
"title": "\(entry.title)",
|
|
"lastVisited": \(now - Double(recencyOffset)),
|
|
"visitCount": \(entry.visitCount)
|
|
"""
|
|
if entry.typedCount > 0 {
|
|
json += """
|
|
,
|
|
"typedCount": \(entry.typedCount),
|
|
"lastTypedAt": \(now - Double(recencyOffset))
|
|
"""
|
|
}
|
|
json += "\n }"
|
|
return json
|
|
}.joined(separator: ",\n")
|
|
|
|
let json = """
|
|
[
|
|
\(entriesJSON)
|
|
]
|
|
"""
|
|
do {
|
|
try json.write(to: url, atomically: true, encoding: .utf8)
|
|
} catch {
|
|
XCTFail("Failed to write browser history seed file: \(error)")
|
|
}
|
|
}
|
|
|
|
private func attachElementDebug(name: String, element: XCUIElement) {
|
|
let payload = """
|
|
identifier: \(element.identifier)
|
|
label: \(element.label)
|
|
exists: \(element.exists)
|
|
hittable: \(element.isHittable)
|
|
frame: \(element.frame)
|
|
"""
|
|
let attachment = XCTAttachment(string: payload)
|
|
attachment.name = name
|
|
attachment.lifetime = .keepAlways
|
|
add(attachment)
|
|
}
|
|
|
|
private func waitForSuggestionRowToBeSelected(_ row: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
self.isSuggestionRowSelected(row)
|
|
}
|
|
}
|
|
|
|
private func isSuggestionRowSelected(_ row: XCUIElement) -> Bool {
|
|
guard row.exists else { return false }
|
|
guard let rawValue = row.value as? String else { return false }
|
|
return rawValue.localizedCaseInsensitiveContains("selected")
|
|
}
|
|
|
|
private func typeQueryAndWaitForSuggestions(
|
|
app: XCUIApplication,
|
|
omnibar: XCUIElement,
|
|
query: String,
|
|
timeout: TimeInterval,
|
|
attempts: Int = 3
|
|
) -> Bool {
|
|
let suggestions = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions").firstMatch
|
|
for _ in 0..<attempts {
|
|
if app.state == .runningBackground {
|
|
app.activate()
|
|
_ = app.wait(for: .runningForeground, timeout: 2.0)
|
|
}
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
guard omnibar.waitForExistence(timeout: 6.0) else { continue }
|
|
omnibar.click()
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
omnibar.click()
|
|
omnibar.typeText(query)
|
|
if suggestions.waitForExistence(timeout: timeout) {
|
|
return true
|
|
}
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
|
}
|
|
return suggestions.exists
|
|
}
|
|
|
|
private func focusOmnibarWithCmdL(app: XCUIApplication, omnibar: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
let attempts = max(1, Int(ceil(timeout)))
|
|
for _ in 0..<attempts {
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
guard omnibar.waitForExistence(timeout: 1.0) else { continue }
|
|
|
|
let before = (omnibar.value as? String) ?? ""
|
|
omnibar.typeText("z")
|
|
|
|
if waitForCondition(timeout: 0.5, predicate: {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
return value != before
|
|
}) {
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
return true
|
|
}
|
|
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func containsExampleDomain(_ value: String) -> Bool {
|
|
value.contains("example.com") || value.contains("example.org")
|
|
}
|
|
|
|
private func waitForCondition(timeout: TimeInterval, predicate: @escaping () -> Bool) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in predicate() },
|
|
object: nil
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
}
|