176 lines
10 KiB
Swift
176 lines
10 KiB
Swift
import XCTest
|
|||
|
|
import UIKit
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class AgentUITests: XCTestCase {
|
||
|
|
let app = XCUIApplication()
|
||
|
|
override func setUp() async throws {
|
||
|
|
continueAfterFailure = false
|
||
|
|
try await control(["action": "reset", "count": 3])
|
||
|
|
app.launchArguments = ["-sybil.ios.apiBaseURL", "http://127.0.0.1:8799/api", "-sybil.ios.adminToken", "fixture-token"]
|
||
|
|
app.launch()
|
||
|
|
openAgent()
|
||
|
|
}
|
||
|
|
private func openAgent() {
|
||
|
|
if !app.buttons["agent-sidebar"].waitForExistence(timeout: 2) || !app.buttons["agent-sidebar"].isHittable {
|
||
|
|
XCTAssertTrue(app.buttons["Show sidebar"].waitForExistence(timeout: 15))
|
||
|
|
app.buttons["Show sidebar"].tap()
|
||
|
|
}
|
||
|
|
XCTAssertTrue(app.buttons["agent-sidebar"].waitForExistence(timeout: 5))
|
||
|
|
app.buttons["agent-sidebar"].tap()
|
||
|
|
XCTAssertTrue(app.textFields["agent-composer"].waitForExistence(timeout: 5) || app.textViews["agent-composer"].exists)
|
||
|
|
}
|
||
|
|
private var composer: XCUIElement {
|
||
|
|
app.textFields["agent-composer"].exists ? app.textFields["agent-composer"] : app.textViews["agent-composer"]
|
||
|
|
}
|
||
|
|
private func send(_ text: String) {
|
||
|
|
composer.tap(); composer.typeText(text); app.buttons["agent-send"].tap()
|
||
|
|
}
|
||
|
|
private func capture(_ name: String) {
|
||
|
|
let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot()); attachment.name = name; attachment.lifetime = .keepAlways
|
||
|
|
add(attachment)
|
||
|
|
}
|
||
|
|
@discardableResult private func control(_ value: [String: Any]) async throws -> [String: Any] {
|
||
|
|
var request = URLRequest(url: URL(string: "http://127.0.0.1:8799/control")!)
|
||
|
|
request.httpMethod = "POST"; request.httpBody = try JSONSerialization.data(withJSONObject: value)
|
||
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||
|
|
return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||
|
|
}
|
||
|
|
func testSendQueueToolsAndRetry() async throws {
|
||
|
|
capture("agent-history")
|
||
|
|
send("first fixture")
|
||
|
|
send("second fixture")
|
||
|
|
XCTAssertTrue(app.staticTexts["second fixture"].waitForExistence(timeout: 5))
|
||
|
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: second fixture")).firstMatch.waitForExistence(timeout: 12))
|
||
|
|
XCTAssertFalse(app.otherElements["agent-typing"].exists)
|
||
|
|
capture("agent-replies-keyboard")
|
||
|
|
app.swipeDown()
|
||
|
|
send("retry fixture")
|
||
|
|
XCTAssertTrue(app.buttons["agent-retry"].waitForExistence(timeout: 5))
|
||
|
|
capture("agent-retry")
|
||
|
|
app.buttons["agent-retry"].tap()
|
||
|
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: retry fixture")).firstMatch.waitForExistence(timeout: 12))
|
||
|
|
send("echo wins fixture")
|
||
|
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: echo wins fixture")).firstMatch.waitForExistence(timeout: 12))
|
||
|
|
XCTAssertFalse(app.buttons["agent-retry"].exists)
|
||
|
|
composer.tap(); app.swipeDown()
|
||
|
|
let failures = app.buttons.matching(identifier: "agent-tool-fixture_error")
|
||
|
|
failures.element(boundBy: failures.count - 1).tap()
|
||
|
|
XCTAssertTrue(app.staticTexts["agent-tool-input"].waitForExistence(timeout: 3))
|
||
|
|
XCTAssertTrue(app.staticTexts["agent-tool-error"].exists)
|
||
|
|
capture("agent-tool-error")
|
||
|
|
let state = try await control(["action": "state"])
|
||
|
|
XCTAssertEqual((state["requests"] as? [String: Any])?.count, 4)
|
||
|
|
}
|
||
|
|
func testReduceMotionSend() async throws {
|
||
|
|
try XCTSkipUnless(UIAccessibility.isReduceMotionEnabled, "Run with Simulator Reduce Motion enabled")
|
||
|
|
send("reduce motion fixture")
|
||
|
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: reduce motion fixture")).firstMatch.waitForExistence(timeout: 12))
|
||
|
|
XCTAssertFalse(app.otherElements["agent-typing"].exists)
|
||
|
|
capture("agent-reduce-motion")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testExpandedToolCollapseToShortHistory() async throws {
|
||
|
|
try await control(["action": "long-tool"])
|
||
|
|
let tool = app.buttons["agent-tool-fixture_long"]
|
||
|
|
XCTAssertTrue(tool.waitForExistence(timeout: 5))
|
||
|
|
tool.tap()
|
||
|
|
XCTAssertTrue(app.staticTexts["agent-tool-result"].waitForExistence(timeout: 3))
|
||
|
|
let scroll = app.scrollViews.firstMatch
|
||
|
|
for _ in 0..<12 {
|
||
|
|
if tool.isHittable { break }
|
||
|
|
scroll.swipeDown()
|
||
|
|
}
|
||
|
|
XCTAssertTrue(tool.isHittable)
|
||
|
|
tool.tap()
|
||
|
|
XCTAssertEqual(tool.value as? String, "Collapsed")
|
||
|
|
try await control(["action": "reply", "content": "Reply after collapsing a tall tool"])
|
||
|
|
XCTAssertTrue(app.staticTexts["Reply after collapsing a tall tool"].waitForExistence(timeout: 5))
|
||
|
|
XCTAssertTrue(app.staticTexts["Reply after collapsing a tall tool"].isHittable)
|
||
|
|
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
|
||
|
|
capture("agent-collapsed-tool-short-history")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testTabletSidebarAndInactiveChat() async throws {
|
||
|
|
try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "iPad layout check")
|
||
|
|
XCUIDevice.shared.orientation = .landscapeLeft
|
||
|
|
XCTAssertTrue(app.buttons["agent-sidebar"].waitForExistence(timeout: 5))
|
||
|
|
XCTAssertTrue(app.buttons["agent-sidebar"].isHittable)
|
||
|
|
app.buttons["New chat"].tap()
|
||
|
|
try await control(["action": "busy", "value": true])
|
||
|
|
try await control(["action": "reply", "content": "iPad inactive chat reply"])
|
||
|
|
let unread = XCTNSPredicateExpectation(predicate: NSPredicate(format: "label CONTAINS %@", "unread replies"), object: app.buttons["agent-sidebar"])
|
||
|
|
await fulfillment(of: [unread], timeout: 5)
|
||
|
|
capture("agent-ipad-sidebar")
|
||
|
|
app.buttons["agent-sidebar"].tap()
|
||
|
|
XCTAssertTrue(app.staticTexts["iPad inactive chat reply"].waitForExistence(timeout: 5))
|
||
|
|
capture("agent-ipad-room-before-send")
|
||
|
|
send("iPad fixture")
|
||
|
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: iPad fixture")).firstMatch.waitForExistence(timeout: 12))
|
||
|
|
capture("agent-ipad-room")
|
||
|
|
XCUIDevice.shared.orientation = .portrait
|
||
|
|
}
|
||
|
|
|
||
|
|
func testScrollPreservationAndKeyboard() async throws {
|
||
|
|
app.terminate()
|
||
|
|
try await control(["action": "reset", "count": 160])
|
||
|
|
app.launch(); openAgent()
|
||
|
|
let scroll = app.scrollViews.firstMatch
|
||
|
|
XCTAssertTrue(app.staticTexts["History message 160: local simulator fixture."].isHittable)
|
||
|
|
scroll.swipeDown()
|
||
|
|
scroll.swipeDown()
|
||
|
|
XCTAssertTrue(app.buttons["agent-jump-latest"].waitForExistence(timeout: 3))
|
||
|
|
let anchor = try XCTUnwrap(app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "History message")).allElementsBoundByIndex.first { $0.isHittable })
|
||
|
|
let before = anchor.frame.minY
|
||
|
|
try await control(["action": "reply", "content": "Reply while reading earlier history"])
|
||
|
|
try await Task.sleep(for: .milliseconds(500))
|
||
|
|
XCTAssertEqual(anchor.frame.minY, before, accuracy: 2)
|
||
|
|
capture("agent-reading-anchor")
|
||
|
|
for _ in 0..<25 {
|
||
|
|
if app.buttons["Load earlier messages"].isHittable { break }
|
||
|
|
scroll.swipeDown(velocity: .fast)
|
||
|
|
}
|
||
|
|
XCTAssertTrue(app.buttons["Load earlier messages"].isHittable)
|
||
|
|
let first = app.staticTexts["History message 61: local simulator fixture."]
|
||
|
|
let beforeLoad = first.frame.minY
|
||
|
|
app.buttons["Load earlier messages"].tap()
|
||
|
|
XCTAssertEqual(first.frame.minY, beforeLoad, accuracy: 3)
|
||
|
|
capture("agent-older-history-anchor")
|
||
|
|
app.buttons["agent-jump-latest"].tap()
|
||
|
|
XCTAssertTrue(app.staticTexts["Reply while reading earlier history"].isHittable)
|
||
|
|
composer.tap(); composer.typeText("A multiline draft\nwith a second line\nand a third line")
|
||
|
|
XCTAssertTrue(app.staticTexts["Reply while reading earlier history"].isHittable)
|
||
|
|
capture("agent-multiline-keyboard")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testBackgroundUnreadReadReconciliationAndReplay() async throws {
|
||
|
|
app.buttons["Show sidebar"].tap()
|
||
|
|
try await control(["action": "busy", "value": true])
|
||
|
|
try await control(["action": "reply", "content": "Late asynchronous fixture reply"])
|
||
|
|
let unread = XCTNSPredicateExpectation(predicate: NSPredicate(format: "label CONTAINS %@", "1 unread replies"), object: app.buttons["agent-sidebar"])
|
||
|
|
await fulfillment(of: [unread], timeout: 5)
|
||
|
|
capture("agent-unread-sidebar")
|
||
|
|
let covered = try await control(["action": "state"])
|
||
|
|
XCTAssertLessThan(covered["readCursor"] as? Int ?? 0, covered["cursor"] as? Int ?? 0)
|
||
|
|
try await control(["action": "read"])
|
||
|
|
let gone = NSPredicate(format: "NOT (label CONTAINS %@)", "unread replies")
|
||
|
|
let cleared = XCTNSPredicateExpectation(predicate: gone, object: app.buttons["agent-sidebar"])
|
||
|
|
await fulfillment(of: [cleared], timeout: 20)
|
||
|
|
app.buttons["agent-sidebar"].tap()
|
||
|
|
XCTAssertTrue(app.staticTexts["Late asynchronous fixture reply"].waitForExistence(timeout: 5))
|
||
|
|
try await control(["action": "disconnect"])
|
||
|
|
try await control(["action": "reply", "content": "Reconnect fixture reply"])
|
||
|
|
XCTAssertTrue(app.staticTexts["Reconnect fixture reply"].waitForExistence(timeout: 10))
|
||
|
|
try await control(["action": "malformed"])
|
||
|
|
try await control(["action": "reply", "content": "Recovered malformed frame"])
|
||
|
|
XCTAssertTrue(app.staticTexts["Recovered malformed frame"].waitForExistence(timeout: 12))
|
||
|
|
XCUIDevice.shared.press(.home)
|
||
|
|
try await control(["action": "reply", "content": "Foreground fixture reply"])
|
||
|
|
app.activate()
|
||
|
|
XCTAssertTrue(app.staticTexts["Foreground fixture reply"].waitForExistence(timeout: 12))
|
||
|
|
XCTAssertEqual(app.staticTexts.matching(NSPredicate(format: "label == %@", "Foreground fixture reply")).count, 1)
|
||
|
|
capture("agent-foreground-replay")
|
||
|
|
}
|
||
|
|
}
|