EchoUITests/ReplyActionsUITests.swift
Rama main · réplica de lectura del repositorio original.
import XCTest
/// Copying a reply says so, and a tool step opens onto what it returned. Taps are the point, so
/// these run in the app.
@MainActor
final class ReplyActionsUITests: XCTestCase {
private func launchDemo() -> XCUIApplication {
continueAfterFailure = false
let app = XCUIApplication()
app.launchArguments = ["-echo.demo", "-echo.demoShort"] + XCUIApplication.chatsSection + XCUIApplication.straightToChat
app.launch()
return app
}
func testCopySaysSo() {
let app = launchDemo()
XCTAssertTrue(app.text(containing: "I've added a reminder").waitForExistence(timeout: 15), "The demo reply didn't appear")
// The actions sit under a finished reply without being asked for.
XCTAssertTrue(app.buttons["Regenerate"].waitForExistence(timeout: 3), "No actions under the reply")
app.buttons["Copy reply"].tap()
let toast = app.staticTexts["Copied"]
XCTAssertTrue(toast.waitForExistence(timeout: 3), "No toast after copying")
XCTAssertTrue(app.buttons["Copied"].exists, "The copy button didn't turn into a check")
let composer = app.textFields.firstMatch.exists ? app.textFields.firstMatch : app.textViews.firstMatch
XCTAssertLessThan(toast.frame.maxY, composer.frame.minY, "The toast sits under the composer")
}
/// While a reply is being worked on its thinking is open, and a tap folds it all the same:
/// it used to stay open, with a header that took no taps, until the reply was done.
func testThinkingFoldsWhileTheReplyIsStillComing() {
let app = XCUIApplication()
app.launchArguments = ["-echo.demoStream"] + XCUIApplication.straightToChat
app.launch()
let hide = app.buttons["Hide thinking"]
XCTAssertTrue(hide.waitForExistence(timeout: 20), "The thinking isn't open while the reply is live")
hide.tap()
XCTAssertTrue(app.buttons["Show thinking"].waitForExistence(timeout: 3), "The tap didn't fold the thinking")
XCTAssertFalse(hide.exists)
// And it stays as the person left it once the reply is done.
XCTAssertTrue(app.buttons["Regenerate"].waitForExistence(timeout: 30), "The demo reply never finished")
XCTAssertTrue(app.buttons["Show thinking"].exists)
}
func testAToolStepOpensOntoWhatItReturned() {
let app = launchDemo()
let steps = app.buttons["Show tool steps"]
XCTAssertTrue(steps.waitForExistence(timeout: 15), "No steps card on the demo reply")
steps.tap()
let step = app.buttons["Tool calendar, completed"]
XCTAssertTrue(step.waitForExistence(timeout: 3), "The card didn't open onto its steps")
XCTAssertFalse(app.text(containing: "Standup").exists, "A step's output is showing before it is opened")
step.tap()
XCTAssertTrue(app.text(containing: "Standup").waitForExistence(timeout: 3), "The step didn't open onto its output")
XCTAssertTrue(app.buttons["Open full output"].exists)
app.buttons["Open full output"].tap()
XCTAssertTrue(app.navigationBars["calendar"].waitForExistence(timeout: 3), "The full output didn't open")
}
/// GitHub issue 46: a long-press on a reply's text selects the word under the finger where
/// it stands, with the selection's menu; "Ask about this" puts it in the composer as a quote.
func testALongPressSelectsInPlaceAndAsksAboutIt() {
let app = launchDemo()
let reply = app.text(containing: "I've added a reminder")
XCTAssertTrue(reply.waitForExistence(timeout: 15), "The demo reply didn't appear")
reply.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5)).press(forDuration: 0.8)
let ask = app.menuItems["Ask about this"].exists ? app.menuItems["Ask about this"] : app.buttons["Ask about this"]
XCTAssertTrue(ask.waitForExistence(timeout: 3), "The long-press didn't select with the selection's menu")
XCTAssertFalse(app.navigationBars["Select text"].exists, "It opened the sheet, not a selection in place")
XCTAssertFalse(app.buttons["Make This a Routine…"].exists || app.buttons["Watch This…"].exists, "The reply's own menu came up instead")
let shot = XCTAttachment(screenshot: app.screenshot())
shot.name = "inline-selection"
shot.lifetime = .keepAlways
add(shot)
// Let go with a tap elsewhere, and press again: the same, the reply drawn as before between.
app.staticTexts["Redde"].firstMatch.tap()
XCTAssertTrue(ask.waitForNonExistence(timeout: 3), "A tap elsewhere didn't let go of the selection")
XCTAssertTrue(reply.waitForExistence(timeout: 3), "The reply wasn't drawn as before")
reply.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5)).press(forDuration: 0.8)
XCTAssertTrue(ask.waitForExistence(timeout: 3), "A second long-press didn't select")
ask.tap()
let composer = app.textFields.firstMatch.exists ? app.textFields.firstMatch : app.textViews.matching(NSPredicate(format: "value BEGINSWITH %@", "> ")).firstMatch
let quoted = NSPredicate(format: "value BEGINSWITH %@", "> ")
expectation(for: quoted, evaluatedWith: composer)
waitForExpectations(timeout: 5)
}
/// The selection's handles move it, as in Notes: the end handle dragged along takes in more.
func testDraggingAHandleGrowsTheSelection() {
let app = launchDemo()
let reply = app.text(containing: "I've added a reminder")
XCTAssertTrue(reply.waitForExistence(timeout: 15), "The demo reply didn't appear")
reply.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5)).press(forDuration: 0.8)
let selection = app.textViews.matching(NSPredicate(format: "identifier BEGINSWITH %@", "inline-selection|")).firstMatch
XCTAssertTrue(selection.waitForExistence(timeout: 3), "The long-press didn't select in place")
func state() -> (length: Int, end: CGPoint) {
let parts = selection.identifier.split(separator: "|").compactMap { Int($0) }
return (parts[0], CGPoint(x: parts[1], y: parts[2]))
}
let before = state()
let first = XCTAttachment(screenshot: app.screenshot())
first.name = "selected \(before.length) end \(Int(before.end.x)),\(Int(before.end.y))"
first.lifetime = .keepAlways
add(first)
let origin = app.coordinate(withNormalizedOffset: .zero)
let handle = origin.withOffset(CGVector(dx: before.end.x, dy: before.end.y + 4))
handle.press(forDuration: 0.15, thenDragTo: origin.withOffset(CGVector(dx: min(before.end.x + 140, 380), dy: before.end.y + 4)))
let shot = XCTAttachment(screenshot: app.screenshot())
shot.name = "handle-dragged"
shot.lifetime = .keepAlways
add(shot)
XCTAssertTrue(selection.waitForExistence(timeout: 2), "The selection ended when its handle was dragged")
let after = state()
XCTAssertGreaterThan(after.length, before.length, "Dragging the end handle didn't take in more text (\(before.length) → \(after.length))")
}
/// The reply's menu is on its ··· button now, "Select text" (the whole reply in a sheet) with it.
func testTheMoreButtonStillHasTheRepliesMenu() {
let app = launchDemo()
XCTAssertTrue(app.buttons["Regenerate"].waitForExistence(timeout: 15), "No actions under the reply")
// The reply's, not the header's.
app.buttons.matching(NSPredicate(format: "label == %@", "More")).allElementsBoundByIndex.last?.tap()
XCTAssertTrue(app.buttons["Select text"].waitForExistence(timeout: 3), "No Select text in the ··· menu")
app.buttons["Select text"].tap()
XCTAssertTrue(app.navigationBars["Select text"].waitForExistence(timeout: 5), "The selectable copy didn't open")
}
/// A double-tap on a reply brings up the reactions; one picked shows under it.
func testADoubleTapReacts() {
let app = launchDemo()
let reply = app.text(containing: "I've added a reminder")
XCTAssertTrue(reply.waitForExistence(timeout: 15), "The demo reply didn't appear")
reply.doubleTap()
let thumbs = app.buttons["React 👍"]
XCTAssertTrue(thumbs.waitForExistence(timeout: 3), "The double-tap didn't bring up the reactions")
thumbs.tap()
XCTAssertTrue(app.buttons["You reacted 👍. Remove"].waitForExistence(timeout: 3), "The reaction doesn't show under the reply")
}
}