import XCTest /// The review and the checklist drawn to the approved designs (asked 2026-10-30), every part of /// them working: a review's Cancel, Edit and its button; a checklist's rows, its ring and its /// groups; each a finger's size, with a VoiceOver hint. `-echo.demoAsks` supplies the cards; the /// answers and ticks start empty each run (`-cardAnswers`, `-cardTicks`). @MainActor final class AnswerCardsUITests: XCTestCase { private func launch(_ extra: [String]) -> XCUIApplication { continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["-echo.demoAsks"] + extra + ["-cardAnswers", "", "-cardTicks", ""] + XCUIApplication.chatsSection + XCUIApplication.straightToChat app.launch() return app } private func button(_ app: XCUIApplication, _ label: String) -> XCUIElement? { app.buttons.matching(NSPredicate(format: "label == %@", label)).hittable(within: 10) } /// A checklist's row: a switch from iOS 27 (its `isToggle` trait), a button before. private func row(_ app: XCUIApplication, _ text: String) -> XCUIElement { app.descendants(matching: .any).matching(NSPredicate(format: "(elementType == %d OR elementType == %d) AND label BEGINSWITH %@", XCUIElement.ElementType.switch.rawValue, XCUIElement.ElementType.button.rawValue, text)).firstMatch } private func assertFingerSized(_ element: XCUIElement, _ name: String) { XCTAssertGreaterThanOrEqual(element.frame.height, 43.5, "\(name) is shorter than a finger") XCTAssertGreaterThanOrEqual(element.frame.width, 43.5, "\(name) is narrower than a finger") } private func keep(_ name: String) { let shot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) shot.name = name shot.lifetime = .keepAlways add(shot) } func testAReviewEditsThenSendsWhatItSays() throws { let app = launch(["-echo.demoAsksReviewOnly"]) XCTAssertTrue(app.text(containing: "Email to send").waitForExistence(timeout: 20), "The review didn't appear") XCTAssertTrue(app.text(containing: "Running a little late tonight").exists, "The subject isn't shown") let edit = try XCTUnwrap(button(app, "Edit"), "No Edit") let cancel = try XCTUnwrap(button(app, "Cancel"), "No Cancel") let send = try XCTUnwrap(button(app, "Send"), "No Send") for (element, name) in [(edit, "Edit"), (cancel, "Cancel"), (send, "Send")] { assertFingerSized(element, name) } // Edit opens the sheet for changes; Done shows it as it will go. edit.tap() let subject = app.textFields["Subject"].exists ? app.textFields["Subject"] : app.textViews["Subject"] XCTAssertTrue(subject.waitForExistence(timeout: 5), "Edit didn't open the fields") keep("review-editing") try XCTUnwrap(button(app, "Done"), "No Done while editing").tap() XCTAssertFalse(app.textFields["Subject"].exists || app.textViews["Subject"].exists, "Done didn't close the fields") // The button sends it, and the card says so. send.tap() XCTAssertTrue(app.text(containing: "Done: Send").waitForExistence(timeout: 10), "Send didn't answer the card") XCTAssertFalse(app.text(containing: "Needs you").exists && app.buttons["Edit"].exists, "The answered card still asks") keep("review-sent") } func testAReviewCancels() throws { let app = launch(["-echo.demoAsksBooking"]) XCTAssertTrue(app.text(containing: "Table at Lupa Trattoria").waitForExistence(timeout: 20), "The booking didn't appear") let cancels = app.buttons.matching(NSPredicate(format: "label == %@", "Cancel")) // The booking's, the last card: on screen. let cancel = try XCTUnwrap(cancels.hittable(within: 10), "No Cancel on screen") cancel.tap() XCTAssertTrue(app.text(containing: "Cancelled").waitForExistence(timeout: 10), "Cancel didn't answer the card") } func testAChecklistTicksLiveAndFolds() throws { let app = launch(["-echo.demoAsksMore", "-echo.demoAsksListOnly"]) XCTAssertTrue(app.text(containing: "Cocoa Beach weekend").waitForExistence(timeout: 20), "The list didn't appear") let ring = { (said: String) in app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", said)).firstMatch } XCTAssertTrue(ring("3 of 8 packed").exists, "The ring doesn't say how it stands") // Each row ticks, and the ring follows. let layer = row(app, "A warm layer") XCTAssertTrue(layer.waitForExistence(timeout: 5), "No row for an item") assertFingerSized(layer, "A row") XCTAssertEqual(layer.value as? String, "Not done") layer.tap() XCTAssertTrue(ring("4 of 8 packed").waitForExistence(timeout: 5), "The ring didn't follow a tick") XCTAssertEqual(layer.value as? String, "Done") XCTAssertNotNil(button(app, "Send"), "A changed list has no Send") keep("checklist-ticked") layer.tap() XCTAssertTrue(ring("3 of 8 packed").waitForExistence(timeout: 5), "The ring didn't follow an untick") for text in ["Swimsuits", "Sandals", "Sunscreen", "Beach towels", "Umbrella", "Phone chargers", "Snacks for the drive"] { XCTAssertTrue(row(app, text).isHittable, "\(text) can't be tapped") } // A group's name folds it away and back. // Folded, it says how many are done too ("Clothes, 1/3"). let clothes = app.buttons.matching(NSPredicate(format: "label ==[c] %@ OR label BEGINSWITH[c] %@", "Clothes", "Clothes,")).firstMatch XCTAssertTrue(clothes.exists, "A group's name isn't a button") assertFingerSized(clothes, "A group's name") clothes.tap() XCTAssertFalse(row(app, "Sandals").waitForExistence(timeout: 2), "The group didn't fold") clothes.tap() XCTAssertTrue(row(app, "Sandals").waitForExistence(timeout: 5), "The group didn't open again") let more = app.buttons["More for Cocoa Beach weekend"] XCTAssertTrue(more.exists, "No menu") assertFingerSized(more, "The menu") } }