cmux/cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
2026-03-17 00:14:28 -07:00

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
}
}