import XCTest /// A conversation's own switches in the model menu, on sample data (`-echo.demoChatControls`), and /// projects in the chat list (`-echo.demoProjects`): "Move to Project" in a row's menu, a new /// conversation in a project that has none yet, and scheduled runs folded away. None of it needs /// a server. @MainActor final class ChatControlsUITests: XCTestCase { private func shot(_ name: String) { let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) attachment.name = name attachment.lifetime = .keepAlways add(attachment) } /// The switch itself, inside its row: a tap on the row's middle lands on the label. private func flip(_ row: XCUIElement) { let knob = row.switches.firstMatch (knob.exists ? knob : row).tap() } private func value(of element: XCUIElement, becomes wanted: String, within timeout: TimeInterval = 5) -> Bool { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if element.value as? String == wanted { return true } RunLoop.current.run(until: Date().addingTimeInterval(0.1)) } return false } func testFastModeAndRunningWithoutAskingInTheModelMenu() { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = XCUIApplication.straightToChat + ["-echo.demoChatControls", "-echo.screen", "model"] app.launch() XCTAssertTrue(app.navigationBars["Model"].waitForExistence(timeout: 20), "The model menu didn't open") let mark = app.descendants(matching: .any).matching(NSPredicate(format: "label == 'Commands run without asking'")).firstMatch XCTAssertFalse(mark.exists, "The header marks a conversation that still asks") let fast = app.switches["Fast mode"] for _ in 0 ..< 6 where !(fast.exists && fast.isHittable) { app.swipeUp() } XCTAssertTrue(fast.exists && fast.isHittable, "No Fast mode switch in the model menu") XCTAssertEqual(fast.value as? String, "0") flip(fast) XCTAssertTrue(value(of: fast, becomes: "1"), "Fast mode didn't turn on") // Running without asking asks first. let unasked = app.switches["Run commands without asking"] XCTAssertTrue(unasked.exists && unasked.isHittable, "No switch for running commands without asking") XCTAssertEqual(unasked.value as? String, "0") flip(unasked) let confirm = app.buttons["Run Without Asking"] XCTAssertTrue(confirm.waitForExistence(timeout: 5), "Turning it on didn't ask first") XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'every command it wants to'")).firstMatch.exists, "The question doesn't say what it means") shot("run without asking confirmation") confirm.tap() XCTAssertTrue(value(of: unasked, becomes: "1"), "The switch didn't turn on") shot("model menu switches") // Back on the chat, the header says so for as long as it is on. app.navigationBars["Model"].swipeDown(velocity: .fast) XCTAssertTrue(mark.waitForExistence(timeout: 8), "Nothing in the header says commands run without asking") shot("header mark") } func testMoveToProjectIsInTheRowsMenu() { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["-echo.demoProjects", "-echo.screen", "sessions"] + XCUIApplication.chatsSection + XCUIApplication.straightToChat app.launch() let row = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Transcript scrolling'")).firstMatch XCTAssertTrue(row.waitForExistence(timeout: 20), "The demo's conversations never appeared") row.press(forDuration: 1.2) let move = app.buttons["Move to Project"] XCTAssertTrue(move.waitForExistence(timeout: 5), "No Move to Project in the row's menu") move.tap() XCTAssertTrue(app.buttons["Echo"].waitForExistence(timeout: 5), "The project isn't offered as a place to move to") XCTAssertTrue(app.buttons["New Project…"].exists, "No way to make a project to move it to") shot("move to project") } func testANewConversationInAnEmptyProjectIsNamedInTheHeader() { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["-echo.demoProjects", "-echo.screen", "sessions"] + XCUIApplication.chatsSection + XCUIApplication.straightToChat app.launch() // Row labels are combined from the whole cell, so match on the leading title. let garden = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Garden'")).firstMatch XCTAssertTrue(garden.waitForExistence(timeout: 20), "A project with no conversations yet isn't in the list") XCTAssertTrue(app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'New Project'")).firstMatch.exists, "No New Project row") shot("projects with new project") garden.tap() XCTAssertTrue(app.navigationBars["Garden"].waitForExistence(timeout: 10), "The project didn't open") XCTAssertTrue(app.staticTexts["No conversations yet"].waitForExistence(timeout: 5), "The empty project says nothing") shot("empty project") let start = app.buttons.matching(identifier: "New Conversation").allElementsBoundByIndex.last { $0.isHittable } XCTAssertNotNil(start, "No New Conversation in the project") start?.tap() let named = app.descendants(matching: .any).matching(NSPredicate(format: "label == 'Project: Garden'")).firstMatch XCTAssertTrue(named.waitForExistence(timeout: 8), "The new conversation's header doesn't name its project") shot("header names the project") } /// GitHub issue 19: runs of scheduled jobs wait folded at the end of the list, and open on a tap. func testScheduledRunsAreFoldedAwayUntilAsked() { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["-echo.demoProjects", "-echo.screen", "sessions"] + XCUIApplication.chatsSection + XCUIApplication.straightToChat app.launch() let chat = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Transcript scrolling'")).firstMatch XCTAssertTrue(chat.waitForExistence(timeout: 20), "The demo's conversations never appeared") let show = app.buttons["Show scheduled runs"] let hide = app.buttons["Hide scheduled runs"] let run = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Morning briefing'")).firstMatch // The fold is remembered: start from folded, whatever a run before left. for _ in 0 ..< 4 where !(show.exists || hide.exists) { app.swipeUp() } if hide.exists { hide.tap() } XCTAssertTrue(show.waitForExistence(timeout: 5), "No Scheduled runs header at the end of the list") XCTAssertFalse(run.exists, "A scheduled run is among the chats") shot("scheduled runs folded") show.tap() XCTAssertTrue(run.waitForExistence(timeout: 5), "Opening Scheduled runs didn't show the runs") shot("scheduled runs open") hide.tap() XCTAssertTrue(show.waitForExistence(timeout: 5), "Scheduled runs didn't fold again") XCTAssertFalse(run.exists, "The runs stayed after folding") } }