630 lines
28 KiB
Swift
630 lines
28 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()
|
|
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
// 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.
|
|
omnibar.typeText("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(waitForRowSelected(row1, timeout: 2.0), "Expected Cmd+N to select row 1. value=\(String(describing: row1.value))")
|
|
|
|
app.typeKey("p", modifierFlags: [.command])
|
|
XCTAssertTrue(waitForRowSelected(row0, timeout: 2.0), "Expected Cmd+P to return to row 0. value=\(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.
|
|
let deadline = Date().addingTimeInterval(8.0)
|
|
while Date() < deadline {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if value.contains("example.com") || value.contains("example.org") {
|
|
return
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
XCTFail("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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
|
|
// Focus omnibar and navigate to example.com via autocompletion (row 0).
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
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.
|
|
func containsExampleDomain(_ value: String) -> Bool {
|
|
value.contains("example.com") || value.contains("example.org")
|
|
}
|
|
|
|
let deadline = Date().addingTimeInterval(8.0)
|
|
while Date() < deadline {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if containsExampleDomain(value) {
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
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"]"#
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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 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(waitForRowSelected(row1, timeout: 2.0), "Expected Cmd+N to select row 1")
|
|
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
XCTAssertTrue(waitForRowSelected(row2, timeout: 2.0), "Expected repeated Cmd+N to keep moving selection")
|
|
|
|
app.typeKey("p", modifierFlags: [.command])
|
|
XCTAssertTrue(waitForRowSelected(row1, timeout: 2.0), "Expected Cmd+P to move selection up")
|
|
}
|
|
|
|
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"]"#
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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.
|
|
let loaded = Date().addingTimeInterval(8.0)
|
|
var loadObserved = false
|
|
while Date() < loaded {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if value.lowercased().contains("example.com") {
|
|
loadObserved = true
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.15))
|
|
}
|
|
XCTAssertTrue(loadObserved, "Expected omnibar to reflect the navigated URL after load. value=\(omnibar.value)")
|
|
|
|
let valueAfterLoad = (omnibar.value as? String) ?? ""
|
|
omnibar.typeText("zx")
|
|
|
|
let typed = Date().addingTimeInterval(5.0)
|
|
var valueCaptured = false
|
|
while Date() < typed {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if value.contains("zx") && value != valueAfterLoad {
|
|
valueCaptured = true
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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 loadedDeadline = Date().addingTimeInterval(8.0)
|
|
var loaded = false
|
|
while Date() < loadedDeadline {
|
|
let value = ((omnibar.value as? String) ?? "").lowercased()
|
|
if value.contains("example.com") || value.contains("example.org") {
|
|
loaded = true
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
|
}
|
|
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")
|
|
|
|
let typedDeadline = Date().addingTimeInterval(4.0)
|
|
var observedValue = ""
|
|
var startsWithTypedPrefix = false
|
|
while Date() < typedDeadline {
|
|
observedValue = ((omnibar.value as? String) ?? "").lowercased()
|
|
if observedValue.hasPrefix("lo") {
|
|
startsWithTypedPrefix = true
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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 row0 = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarSuggestions.Row.0").firstMatch
|
|
XCTAssertTrue(row0.waitForExistence(timeout: 4.0))
|
|
|
|
let row0Value = (row0.value as? String) ?? ""
|
|
XCTAssertTrue(
|
|
row0Value.localizedCaseInsensitiveContains("gmail"),
|
|
"Expected autocomplete candidate to be first row. row0Value=\(row0Value)"
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
let deadline = Date().addingTimeInterval(8.0)
|
|
var committedToGmail = false
|
|
while Date() < deadline {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if value.localizedCaseInsensitiveContains("gmail.com") {
|
|
committedToGmail = true
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
|
}
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
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"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0))
|
|
omnibar.typeText("go")
|
|
|
|
let inlineDeadline = Date().addingTimeInterval(3.0)
|
|
while Date() < inlineDeadline {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
if value.contains("google.com") {
|
|
break
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
XCTAssertTrue(
|
|
((omnibar.value as? String) ?? "").contains("google.com"),
|
|
"Expected inline completion to show google.com before Cmd+A."
|
|
)
|
|
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.25))
|
|
|
|
let afterCmdA = (omnibar.value as? String) ?? ""
|
|
XCTAssertTrue(
|
|
afterCmdA.contains("google.com"),
|
|
"Expected Cmd+A to preserve inline completion display instead of collapsing to typed prefix. value=\(afterCmdA)"
|
|
)
|
|
}
|
|
|
|
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 waitForRowSelected(_ row: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
let deadline = Date().addingTimeInterval(timeout)
|
|
while Date() < deadline {
|
|
if ((row.value as? String) ?? "").contains("selected") {
|
|
return true
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
|
}
|
|
return ((row.value as? String) ?? "").contains("selected")
|
|
}
|
|
}
|