import XCTest /// GitHub issue 21: a card's question, a share and a Siri draft can start a new conversation /// instead of going into the open one. On the demo conversation, with no server: the OpenAI- /// compatible connection pointed at nothing, so a question is sent (and fails) without one. @MainActor final class NewConversationEntryUITests: XCTestCase { private func launch(_ extra: [String]) -> XCUIApplication { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["-echo.demo", "-echo.demoShort", "-echo.clearDrafts", "-transport", "chatCompletions", "-fastLaneURL", "http://127.0.0.1:9", "-fastLaneModel", "test"] + extra + XCUIApplication.straightToChat app.launch() return app } /// Drafts outlast the app, and other tests start from an empty message field: what a share or /// a Siri draft left here would be in front of their text. And the card's conversation goes, /// so the next run starts as this one did. (Not through the list: its question waits to be /// sent, and each try at sending it closes the list.) override func tearDownWithError() throws { MainActor.assumeIsolated { let app = XCUIApplication() app.launchArguments = ["-echo.clearDrafts", "-echo.forget", "About kanban card"] + XCUIApplication.straightToChat app.launch() _ = app.textFields.firstMatch.waitForExistence(timeout: 10) sleep(1) app.terminate() } } /// The demo conversation's first message, in the transcript. private func question(_ app: XCUIApplication) -> XCUIElement { app.text(containing: "What's on my calendar tomorrow") } /// The start screen, shown only on an empty conversation. private func startScreen(_ app: XCUIApplication) -> XCUIElement { app.text(containing: "What should Redde look into") } /// What the message field holds, once it holds `fragment`. private func composerHolds(_ fragment: String, in app: XCUIApplication) -> Bool { let field = app.textFields.firstMatch guard field.waitForExistence(timeout: 20) else { return false } let deadline = Date().addingTimeInterval(10) while Date() < deadline { if (field.value as? String)?.contains(fragment) == true { return true } RunLoop.current.run(until: Date().addingTimeInterval(0.25)) } return false } func testACardAskedInANewConversationLeavesTheOpenOne() { let app = launch(["-echo.demoKanban", "-echo.screen", "sessions", "-echo.section", "kanban"]) let card = app.text(containing: "Compare heat pump quotes") XCTAssertTrue(card.waitForExistence(timeout: 20), "The demo board isn't on screen") card.tap() let fresh = app.buttons["Ask in a new conversation"] XCTAssertTrue(fresh.waitForExistence(timeout: 10), "No way to ask in a new conversation") XCTAssertTrue(app.text(containing: "Ask in the open conversation").exists, "No way to ask in the open one") fresh.tap() XCTAssertTrue(app.text(containing: "About kanban card").waitForExistence(timeout: 10), "The card's question wasn't sent") XCTAssertFalse(question(app).exists, "It went into the open conversation") } /// Hermes's other boards (GitHub issue 65): the switcher over the columns names the board /// shown, and picking another shows that board's cards. func testAnotherKanbanBoardCanBePicked() { let app = launch(["-echo.demoKanban", "-echo.screen", "sessions", "-echo.section", "kanban"]) XCTAssertTrue(app.text(containing: "Compare heat pump quotes").waitForExistence(timeout: 20), "The demo board isn't on screen") let switcher = app.descendants(matching: .any).matching(NSPredicate(format: "label == 'Board: Household'")).firstMatch XCTAssertTrue(switcher.waitForExistence(timeout: 5), "No board switcher over the columns") switcher.tap() let garden = app.buttons.matching(NSPredicate(format: "label CONTAINS 'Garden'")).firstMatch XCTAssertTrue(garden.waitForExistence(timeout: 5), "The other board isn't offered") garden.tap() XCTAssertTrue(app.text(containing: "Order the tomato seeds").waitForExistence(timeout: 10), "The other board's cards didn't show") XCTAssertFalse(app.text(containing: "Compare heat pump quotes").exists, "The first board's cards are still up") XCTAssertTrue(app.descendants(matching: .any).matching(NSPredicate(format: "label == 'Board: Garden'")).firstMatch.exists, "The switcher doesn't name the board picked") } func testACardAskedInTheOpenConversationStaysThere() { let app = launch(["-echo.demoKanban", "-echo.screen", "sessions", "-echo.section", "kanban"]) let card = app.text(containing: "Compare heat pump quotes") XCTAssertTrue(card.waitForExistence(timeout: 20), "The demo board isn't on screen") card.tap() let open = app.text(containing: "Ask in the open conversation") XCTAssertTrue(open.waitForExistence(timeout: 10), "No way to ask in the open conversation") open.tap() XCTAssertTrue(app.text(containing: "About kanban card").waitForExistence(timeout: 10), "The card's question wasn't sent") XCTAssertTrue(question(app).exists, "It didn't go into the open conversation") } func testAShareWithNewConversationWaitsInANewOne() { let app = launch(["-echo.shareNew", "Shared from Safari"]) XCTAssertTrue(startScreen(app).waitForExistence(timeout: 20), "The share kept the old conversation") XCTAssertTrue(composerHolds("Shared from Safari", in: app), "The share isn't in the new conversation's message field") // Back in the one it left (the start screen's Continue card): the share didn't stay // behind as its draft too. let back = app.text(containing: "Continue: What's on my calendar tomorrow") XCTAssertTrue(back.waitForExistence(timeout: 10), "No way back to the conversation it left") back.tap() XCTAssertTrue(question(app).waitForExistence(timeout: 10), "The demo conversation didn't open") let field = app.textFields.firstMatch XCTAssertTrue(field.waitForExistence(timeout: 10), "No message field") XCTAssertFalse((field.value as? String)?.contains("Shared from Safari") == true, "The share stayed behind in the old conversation too") } func testAShareWithoutItWaitsInTheOpenOne() { let app = launch(["-echo.share", "Shared from Safari"]) XCTAssertTrue(composerHolds("Shared from Safari", in: app), "The share isn't in the message field") XCTAssertTrue(question(app).exists, "The share left the open conversation") } func testASiriDraftWaitsInANewConversationWhenSoSet() { let app = launch(["-siriAIEnabled", "YES", "-siriDraftsInNewConversation", "YES", "-echo.siriDraft", "Drafted by Siri"]) XCTAssertTrue(startScreen(app).waitForExistence(timeout: 20), "The draft kept the old conversation") XCTAssertTrue(composerHolds("Drafted by Siri", in: app), "The draft isn't in the new conversation's message field") } }